Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)

홀 펀칭(Hole Punching)을 이용한 Private IP 간 통신 - C#


오... 재미있는 사실을 하나 알았습니다. ^^

실전에서 알아보는 홀펀칭 방법.
; http://www.gamedevforever.com/47

간단한 예를 들어서, 가정에서 공유기를 이용하여 인터넷에 접속한 A, B 사용자가 있다고 가정할 때 대부분 공유기에 공용 IP가 할당되기 때문에 서로 간에 통신이 되지 않습니다. 그럴 때 홀 펀칭을 이용해주면 A, B 모두 Private IP를 사용하고 있는데도 불구하고 서로 간에 메시지를 보낼 수 있습니다. (사설 네트워크는 A, B, C 클래스 별로 각각 10.0.0.0, 172.16.0.0, 192.168.0.0을 예약)

테스트를 하기 위해 서버 측 역할을 하는 컴퓨터 한 대와, 각각의 영역에서 Private IP를 가지고 있는 PC 2대가 필요합니다. 실제로 어떻게 패킷이 이동하는지 따라가 볼까요? ^^

우선, 클라이언트가 서버 측에 UDP 메시지를 전송할 것입니다. 이 과정에서 다음과 같은 연결 통로가 구성됩니다.

hole_punch_1.png

실제로 위와 같은 그림의 통신이 발생하도록 C# 코드로 구성해볼까요? ^^

일단 서버의 UDP 소켓은 12000 번 포트로 입력을 대기하는 것으로 시작해야 합니다.

===== 서버 측 소스 코드 =====
UdpClient _server;
_server = new UdpClient(12000);
_server.BeginReceive(udpReceiveCallback, _server); // 비동기 데이터 수신

void udpReceiveCallback(IAsyncResult ar)
{
    try
    {
        UdpClient udpServer = ar.AsyncState as UdpClient;
        IPEndPoint remoteEndPoint = null;

        byte [] receiveBytes = udpServer.EndReceive(ar, ref remoteEndPoint);

        // 접속된 클라이언트의 IP 주소와 포트 출력
        Console.WriteLine("Receive from " + remoteEndPoint.Address.ToString() + ":" + remoteEndPoint.Port);
        udpServer.BeginReceive(udpReceiveCallback, udpServer);
    }
    catch { }
}

보시는 것처럼, 서버는 현재 단순하게 접속된 클라이언트의 IP 주소와 포트를 출력하는 기능만 있습니다.

이어서 클라이언트 측을 구현하면 다음과 같습니다.

===== 클라이언트 측 소스 코드 =====
UdpClient _udpClient = new UdpClient();

private void Form1_Load(object sender, EventArgs e)
{
    IPAddress ipAddress = IPAddress.Parse("124.137.26.136");
    IPEndPoint holePunchServer = new IPEndPoint(ipAddress, 12000);

    string uid = Environment.MachineName;
    byte [] uidBytes = Encoding.UTF8.GetBytes(uid);

    _udpClient.Send(uidBytes, uidBytes.Length, holePunchServer);
}

위와 같이 클라이언트를 실행하고 서버로 데이터가 전송되면 서버 측에는 어떤 메시지가 출력될까요? 당연히 "Receive from 175.194.21.149:60010"이 됩니다. 통신 패킷만으로 보면 서버 측에서는 절대 클라이언트의 Private IP와 포트를 알 수 없습니다.

이 때문에, 서버에서 클라이언트로 데이터를 보내야 할 일이 생기면 공유기에 열린 포트를 대상으로 데이터를 전송하게 됩니다. 아래의 그림에서 보는 것처럼, 다시 그 과정이 역으로 진행되는 것입니다.

hole_punch_2.png

이 때 공유기는 60010 포트로 들어온 데이터가 수신되어야 할 내부 IP의 컴퓨터에 대한 정보(192.168.50.10, 50010포트)를 가지고 있으므로 정상적으로 해당 컴퓨터로 UDP 패킷을 전송할 수 있게 되는 것입니다.




이론적으로는 저렇게 패킷이 오고가는 것은 알고 있었지만... "Hole Punching"까지는 생각할 수 없었습니다. 역시 ... 이런 맛에 공부해나가는 즐거움이 있겠지요. ^^

"Hole Punching"에서는 공유기의 NAT에서 유지되는 매핑 테이블의 도움을 받아 이뤄집니다. 아이디어는 사실 매우 간단합니다. 즉, UDP 서버가 아니라 다른 컴퓨터에서 해당 공유기 IP의 60010 포트로 데이터를 전송하면 어떨까 하는 것입니다.

hole_punch_3.png

물론, 저 상황에서는 데이터를 보낸 측이 "124.137.26.36:12000" 주소가 아니기 때문에 공유기 측에서 버려집니다. 하지만, 저렇게 데이터를 보내는 와중에 클라이언트 측이 "100.100.100.100:15000" 주소로 데이터를 한번 전송해 주면 어떻게 될까요?

hole_punch_4.png

공유기 매핑 테이블에는 175.194.21.14:60010 포트에 대해 2가지 원격 주소지가 포함되어 데이터를 받을 수 있게 됩니다. 결과적으로, Private IP를 가지고 있는 PC 임에도 불구하고 마치 공용 IP에 연결할 수 있는 것처럼 데이터를 전달받을 수 있게 된 것입니다.

어떠세요? 처음 이 글을 봤을 때 '아~~~' 하는 감탄사가 나오더군요. ^^




일단, 원리는 그렇다 치고 정말 되는지 한번 확인을 해봐야 되지 않을까요? ^^

그래서, 테스트 환경을 준비해 봤습니다. 우선, 제가 테스트 해 볼 수 있는 network가 '회사 컴퓨터'와 '집'입니다. 물론, 집에 있는 컴퓨터는 Access Point를 통해서 연결하고 있기 때문에 Private IP입니다. 그런데... 다른 하나의 클라이언트를 대신해 줄 네트워크가 마땅치 않군요. 하지만, 마침 이전에 ^^ 만들어 두었던 아마존 EC2 무료 Windows 서버 Virtual Machine이 생각났습니다.

아마존 EC2에 새로 추가된 "1년 무료 Windows 서버 인스턴스"가 있다는데, 직접 만들어 볼까요? ^^
; https://www.sysnet.pe.kr/2/0/1224

이렇게 해서 3군데의 네트워크를 확보하고, 서버/클라이언트는 다음과 같이 정했습니다.

회사 컴퓨터: UDP 서버
집 컴퓨터: Hole Punching을 하게 되는 클라이언트 A
아마존 가상 컴퓨터: Hole Punching을 하게 되는 클라이언트 B

아마존 가상 컴퓨터 역시 Private IP를 가지고 있습니다. 과연, 아마존 가상 컴퓨터와 집에 있는 컴퓨터 간에 UDP 통신이 가능할까요? 와~~~ 저도 궁금해집니다. ^^

우선, 첨부 파일을 내려 받아서 빌드하면 다음의 2가지 프로젝트가 나옵니다.

HolePunchServer: UDP 서버
HolePunchClient: Hole Punching을 이용해 서로 통신할 UDP 클라이언트

빌드한 후, HolePunchServer를 회사 컴퓨터에서 실행시킵니다. 기본값이 12800 포트로 대기하고 있기 때문에 만약 여러분들의 환경에 맞지 않다면 변경해 주시면 됩니다.

hole_punch_5.png

그다음, 각각 아마존과 집 컴퓨터에서 HolePunchClient를 실행시킵니다. (빌드하기 전에, app.config 안의 SERVER_HOST, SERVER_PORT 값을 자신이 테스트 하는 UDP 서버의 값으로 바꿔주어야 합니다.)

hole_punch_6.png

이제 양쪽에서 Connect 버튼을 누르면 회사의 UDP 서버로 데이터를 전송합니다. 이 과정에서 UDP 서버는 양쪽 컴퓨터의 Public IP와 포트를 취합하게 되고, 서로 통신을 할 양쪽의 IP/Port 정보를 클라이언트 측에 내려줍니다.

hole_punch_7.png

여기까지야 뭐... 일반적인 Server / Client 통신일 뿐이니 신기할 것이 없습니다. 이제, 분배받은 상대방 NAT 장비에 지정된 포트로 데이터를 전송하기 위해 "Send" 버튼을 누릅니다.

hole_punch_8.png

오~~~ 정말, 서로의 NAT 장비에 뚫어놓았던 UDP 서버에 연결된 포트 번호로 데이터를 전송하니 양측에서 데이터를 송/수신하고 있습니다. 게다가 서버 측에서 계속 내려주는 LIST 정보까지 받는 걸로 봐서 공유기가 다수의 컴퓨터에서 오는 데이터를 정상적으로 UDP 클라이언트에 전달해 주는 것을 알 수 있습니다.

직접 해보니.. 정말 신기하군요. ^^




아쉬운 점이라면, 이러한 Hole Punching이 100% 되는 것은 아닙니다. 원문에도 언급하고 있지만 해외 서비스 통계상으로 80% 이상의 성공율을 보이고 있다고 합니다. 또한 공유기의 특성에 따라 매핑된 IP:포트 테이블을 3초만에 지우는 경우도 있다고 하고 지원되는 NAT의 다양한 방식으로 인해 (Full Cone, Restricted Cone, Port Restricted Cone, Symmetric) 대응도 다를 수 있다고 합니다.

물론, 안 되는 경우를 위해서 UDP 서버 측에서 직접 메시지 전달을 대행해 주는 코드를 만들어야 겠지만... 그래도 80% 정도의 클라이언트에 대한 네트워크 통신 부하를 서버로부터 없앨 수 있다는 것은 대단한 장점입니다.

좀 더 기술적인 부분에 대해서는 다음의 글을 참고하십시오.

Peer-to-Peer Communication Across Network Address Translators 
; https://docs.google.com/View?id=dc65vhw7_118g5dp5xf3

Hole Punching
; http://sweeper.egloos.com/2431396

참고로, 이 글에서는 UDP만 설명했지만 TCP도 가능합니다. 게다가 이 글에 첨부한 프로젝트는 간단하게 테스트를 위해 만들어본 코드라서 여러분들의 환경에 맞게 변경해야 하지만, 다음의 공개 C# 소스 코드를 이용하시면 그런 부담을 덜을 수 있습니다.

Introduction to SharpSTUNT
; http://sharpstunt.codeplex.com/

이렇게 재미있는 기법을 게임 프로그래머들은 이미 다 알고 있었군요. ^^




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/22/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2012-02-04 04시44분
[lawrence kim] STUNT가 C#으로도 있었네요 좋은 정보 얻어갑니다 ^^
[guest]
2012-02-04 05시26분
"lawrence kim" 님도 이미 홀펀칭을 알고 계셨군요. ^^
정성태
2012-02-08 04시26분
Virtual Networking 환경을 만들어주는 LogMein Hamachi 프로그램이 말씀하신 홀펀칭을 이용했던 것이군요... PC 끼리 방화벽/공유기 통과해서 데이터 통신을 하는게 신기했었는데.. 감사합니다.
스포너
2012-02-08 04시46분
와~~~ 말씀하신 LogMeIn 제품, 상당히 매력적이군요. 좋은 정보 감사드립니다. ^^
정성태
2012-06-15 07시33분
[윤형선] 안녕하세요. 저는 홀펀칭을 공부중인 학생입니다.
홀펀칭이라는 기술을 공부하다 보니 의문점이 있어서 질문하나 드릴게요~^^;

NAT A에 두개의 클라이언트가 물려 있다고 가정하면 어떠한 클라이언트에게
어떠한 data(infomation)를 보고 어떻게 해당 클라이언트를 찾아 들어가는지 궁금합니다.

자세히 다시 설명드리면
지금까지 제가 공부한 바로는..
1. Client(A)는 서버에게 P2P 통신을 하려는 Client (B)의 Public Ip, Port / Private Ip, Port를 받는다.
2. 서버로 부터 받아온 Public/Private 주소에 UDP 패킷을 보내고
   if Private 주소에서 받으면 direct로 통신
   if Public 주소에서 받으면, 받은 NAT(B)에서는 받은 패킷의 정보가 없기 때문에 버려진다.
3. Client (B)에서도 Client(A)로 UDP 패킷을 보낸다.
4. NAT(A)에는 이미 NAT(B)로 패킷을 보냈던 정보가 저장이 되어 있기 때문에 Client (A)에게 데이터가 전달된다.

라고 이해를 하고 있습니다. 하지만 이 상황에서 위에 말씀드렸다 싶이 NAT(A)에 두개 이상의 Client가 있다면,
이 Client들 중에 패킷을 보냇던 Client를 어떻게 구별을 하는지 궁금합니다.
혹시 시간이 되신다면 답변해주시면 감사하겠습니다.
그럼 수고하세요..
[guest]
2012-06-16 10시31분
윤형선 님의 질문을 일단 제가 이해한대로 답변을 해보겠습니다.

쉬운 예로, 채팅을 한다고 가정할 때 NAT(A: 192.168.0.1)에 Alice(192.168.0.2:포트3000), Bob(192.168.0.3:포트6000) 사용자가 묶여 있습니다. 그 두 사용자가 홀펀칭 서버(100.100.100.100)에 접속했다고 가정해 보겠습니다. 그럼, NAT(A)에는 다음과 같은 정보가 들어있습니다.

192.168.0.2:포트3000 <-> 192.168.0.1:포트9000, 192.168.0.1:포트9000 <-> 100.100.100.100:포트8000
192.168.0.3:포트6000 <-> 192.168.0.1:포트9001, 192.168.0.1:포트9001 <-> 100.100.100.100:포트8000

이런 상태에서, Tom이라는 사용자가 Alice와 채팅을 하고 싶다면, 홀펀칭 서버로부터 Alice가 192.168.0.1:포트9000에 매핑된 것을 확인할 수 있습니다. 마찬가지로 Bob이라면 192.168.0.1:포트9001임을 알 수 있고.

설명이 되었나요? ^^
정성태
2012-06-28 04시31분
[알렉스] 안녕하세요, 정성태님.
좋은 글에 감사를 드립니다. 하지만 소스코드와 작성하신 글의 내용이 이해가 잘 안되는 부분이 있어 문의드립니다.

글에는

"물론, 저 상황에서는 데이터를 보낸 측이 "124.137.26.36:12000" 주소가 아니기 때문에 공유기 측에서 버려집니다. 하지만, 저렇게 데이터를 보내는 와중에 클라이언트 측이 "100.100.100.100:15000" 주소로 데이터를 한번 전송해 주면 어떻게 될까요?"

즉 클라이언트가 100.100.100.100:15000으로 데이터를 한번 전송해 주어야 NAT에 매핑 테이블에 기록이 되어 100.100.100.100:15000으로부터 클라이언트에게 데이터를 보낼 수 있다는 것인데, 100.100.100.100:15000이 만일 NAT아래에 있다면 클라이언트가 패킷을 보낼 수도 없을텐데 그러한경우엔 어떻게 하는건지 궁금합니다.
[guest]
2012-06-28 12시44분
100.100.100.100:15000이 NAT 아래에 있다고 해도 상관없습니다. 결국 그 NAT도 192.168.50.10 컴퓨터를 중계하고 있는 NAT처럼 동작할 것이기 때문에 포트가 열리게 됩니다. 175.194.21.149 NAT처럼 100.100.100.100에도 NAT을 하나 넣어서 IP 매핑 테이블을 직접 그려 보시면 왜 그런지 금방 아시게 될 것입니다.

단지, 문제가 된다면 100.100.100.100과 192.168.50.10 컴퓨터가 같은 NAT에 속해 있을 경우입니다. 바로 그 경우가 홀펀칭이 잘 안되는 대표적인 사례입니다. 위에서 제가 참고하라는 글 중에 "Peer-to-Peer Communication Across Network Address Translators"의 "3.3 Peers Behind a Common NAT" 내용을 보시면 이에 대해 자세하게 설명되어 있는데요. 이런 경우, 해당 NAT이 'hairpin' 변환을 지원하면 패킷이 전송되는데, 현실적으로 볼 때 대부분의 NAT에서 hairpin 변환이 지원되지 않는다고 합니다.

따라서 이런 경우를 포함해서 홀펀칭이 안되는 상황을 고려해 별도의 릴레이 역할을 하는 서버를 하나 두어야 합니다. "실전에서 알아보는 홀펀칭 방법." 글에서도 나오지만 홀펀칭이 100% 되지는 않습니다. ^^
정성태
2013-10-07 06시43분
[완료] 각기 다른 사설망(NAT)에 들어있는 클라이언트를 서로 연결해주기 (메신저)
; http://kldp.org/node/122704
정성태
2015-08-04 01시03분
결국 A에서 B로 공인IP 및 포트로 한번 연결 시도하면 상대방은 A로 연결할 때 A가 B로 연결시도 했기 때문에 만들어진 공유기의 홀을 통해 접속이 가능한 거네요. 예전에 네트워크 공부할 때 사설망끼리의 P2P는 어떻게 연결이 가능한지 궁금했었는데 말끔히 해소되었습니다.^^ 좋은 글 감사합니다.
Beren Ko
2016-07-06 01시49분
국내 LTE 통신사별 P2P 홀펀칭 성공율
; http://lab.gamecodi.com/board/zboard.php?id=GAMECODILAB_Proudnet_Lec&no=12&z=
정성태
2018-01-31 03시00분
[물벼주] 소켓이 연결되어 있지 않거나 sendto 호출을 사용하여 데이터그램 소켓에 보내는 경우에 주소가 제공되지 않아서 데이터를 보내거나 받도록 요청할 수 없습니다. 라고 나오는건 뭘 확인해야 할까요?
[guest]
2021-01-23 07시33분
NAT를 넘어서 가자 (.NET Conf 2021 - 17분 동영상)
; https://www.youtube.com/watch?v=NK5lxhwt8Nc
정성태
2023-03-06 09시23분
[김성호] 예제 프로그램 되는건가요? 방화벽까지 다 확인했는데요. HolePunchClient1 에서 HolePunchServer에 접속 조차 안되는데요.
[guest]
2023-03-07 07시58분
HolePunchClient1에서 HolePunchServer로 접속하는 것은 단순한 UDP 통신입니다. 꼭 이 글의 예제가 아니더라도 UDP 통신이 안 된다는 것이니 네트워크 구성을 좀 더 살펴보세요.

참고로, 이 글의 예제를 꼭 따를 필요는 없습니다. 소스 코드 보시면 금방 이해할 정도의 단순한 UDP 통신에 불과하므로, 직접 만드셔도 크게 어렵진 않을 것입니다.
정성태

1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13576정성태3/8/20241543닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
13575정성태3/7/20241676닷넷: 2227. 최신 C# 문법을 .NET Framework 프로젝트에 쓸 수 있을까요?
13574정성태3/6/20241557닷넷: 2226. C# - "Docker Desktop for Windows" Container 환경에서의 IPv6 DualMode 소켓
13573정성태3/5/20241563닷넷: 2225. Windbg - dumasync로 분석하는 async/await 호출
13572정성태3/4/20241641닷넷: 2224. C# - WPF의 Dispatcher Queue로 알아보는 await 호출의 hang 현상파일 다운로드1
13571정성태3/1/20241619닷넷: 2223. C# - await 호출과 WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13570정성태2/29/20241632닷넷: 2222. C# - WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13569정성태2/28/20241545닷넷: 2221. C# - LoadContext, LoadFromContext 그리고 GAC파일 다운로드1
13568정성태2/27/20241606닷넷: 2220. C# - .NET Framework 프로세스의 LoaderOptimization 설정을 확인하는 방법파일 다운로드1
13567정성태2/27/20241617오류 유형: 898. .NET Framework 3.5 이하에서 mscoree.tlb 참조 시 System.BadImageFormatException파일 다운로드1
13566정성태2/27/20241630오류 유형: 897. Windows 7 SDK 설치 시 ".NET Development" 옵션이 비활성으로 선택이 안 되는 경우
13565정성태2/23/20241478닷넷: 2219. .NET CLR2 보안 모델에서의 개별 System.Security.Permissions 제어
13564정성태2/22/20241614Windows: 259. Hyper-V Generation 1 유형의 VM을 Generation 2 유형으로 바꾸는 방법
13563정성태2/21/20241644디버깅 기술: 196. windbg - async/await 비동기인 경우 메모리 덤프 분석의 어려움
13562정성태2/21/20241644오류 유형: 896. ASP.NET - .NET Framework 기본 예제에서 System.Web에 대한 System.IO.FileNotFoundException 예외 발생
13561정성태2/20/20241743닷넷: 2218. C# - (예를 들어, Socket) 비동기 I/O에 대한 await 호출 시 CancellationToken을 이용한 취소파일 다운로드1
13560정성태2/19/20241746디버깅 기술: 195. windbg 분석 사례 - Semaphore 잠금으로 인한 Hang 현상 (닷넷)
13559정성태2/19/20242623오류 유형: 895. ASP.NET - System.Security.SecurityException: 'Requested registry access is not allowed.'
13558정성태2/18/20241819닷넷: 2217. C# - 최댓값이 1인 SemaphoreSlim 보다 Mutex 또는 lock(obj)를 선택하는 것이 나은 이유
13557정성태2/18/20241619Windows: 258. Task Scheduler의 Author 속성 값을 변경하는 방법
13556정성태2/17/20241684Windows: 257. Windows - Symbolic (hard/soft) Link 및 Junction 차이점
13555정성태2/15/20241952닷넷: 2216. C# - SemaphoreSlim 사용 시 주의점
13554정성태2/15/20241708VS.NET IDE: 189. Visual Studio - 닷넷 소스코드 디컴파일 찾기가 안 될 때
13553정성태2/14/20241735닷넷: 2215. windbg - thin/fat lock 없이 동작하는 Monitor.Wait + Pulse
13552정성태2/13/20241685닷넷: 2214. windbg - Monitor.Enter의 thin lock과 fat lock
13551정성태2/12/20242016닷넷: 2213. ASP.NET/Core 웹 응용 프로그램 - 2차 스레드의 예외로 인한 비정상 종료
1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...