Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

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


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

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

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

테스트를 하기 위해 서버 측 역할을 하는 컴퓨터 한 대와, 각각의 영역에서 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/

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




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

[연관 글]


donaricano-btn



[최초 등록일: ]
[최종 수정일: 1/22/2021 ]

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

비밀번호

댓글 쓴 사람
 



2012-02-04 04시44분
[lawrence kim] STUNT가 C#으로도 있었네요 좋은 정보 얻어갑니다 ^^
[손님]
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를 어떻게 구별을 하는지 궁금합니다.
혹시 시간이 되신다면 답변해주시면 감사하겠습니다.
그럼 수고하세요..
[손님]
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아래에 있다면 클라이언트가 패킷을 보낼 수도 없을텐데 그러한경우엔 어떻게 하는건지 궁금합니다.
[손님]
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 호출을 사용하여 데이터그램 소켓에 보내는 경우에 주소가 제공되지 않아서 데이터를 보내거나 받도록 요청할 수 없습니다. 라고 나오는건 뭘 확인해야 할까요?
[손님]
2021-01-23 07시33분
NAT를 넘어서 가자 (.NET Conf 2021 - 17분 동영상)
; https://www.youtube.com/watch?v=NK5lxhwt8Nc
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12547정성태3/3/20215오류 유형: 699. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly.
12546정성태3/3/202125개발 환경 구성: 545. github workflow/actions에서 빌드시 snk 파일 다루는 방법 - Encrypted secrets
12545정성태3/2/202193.NET Framework: 1025. 닷넷 5에 추가된 POH (Pinned Object Heap) [3]
12544정성태2/27/2021137.NET Framework: 1024. C# - Control의 Invalidate, Update, Refresh 차이점 [2]
12543정성태2/26/2021111VS.NET IDE: 1577. C# - 디자인 타임(design-time)과 런타임(runtime)의 코드 실행 구분
12542정성태3/3/2021164개발 환경 구성: 544. github repo의 Release 활성화 및 Actions를 이용한 자동화 방법
12541정성태2/18/2021217개발 환경 구성: 543. 애저듣보잡 - Github Workflow/Actions 소개
12540정성태2/17/2021205.NET Framework: 1023. C# - Win32 API에 대한 P/Invoke를 대신하는 Microsoft.Windows.CsWin32 패키지
12539정성태2/16/2021268Windows: 188. WM_TIMER의 동작 방식 개요파일 다운로드1
12538정성태2/16/2021316.NET Framework: 1022. C# - GC 힙이 아닌 Native 힙에 인스턴스 생성 - 0SuperComicLib.LowLevel 라이브러리 소개 [2]
12537정성태2/11/2021385.NET Framework: 1021. UI 요소의 접근은 반드시 그 UI를 만든 스레드에서! - 두 번째 이야기
12536정성태2/9/2021213개발 환경 구성: 542. BDP(Bandwidth-delay product)와 TCP Receive Window
12535정성태2/10/2021173개발 환경 구성: 541. Wireshark로 확인하는 LSO(Large Send Offload), RSC(Receive Segment Coalescing) 옵션
12534정성태2/17/2021187개발 환경 구성: 540. Wireshark + C/C++로 확인하는 TCP 연결에서의 closesocket 동작파일 다운로드1
12533정성태2/8/2021212개발 환경 구성: 539. Wireshark + C/C++로 확인하는 TCP 연결에서의 shutdown 동작파일 다운로드1
12532정성태2/6/2021209개발 환경 구성: 538. Wireshark + C#으로 확인하는 ReceiveBufferSize(SO_RCVBUF), SendBufferSize(SO_SNDBUF)
12531정성태2/5/2021169개발 환경 구성: 537. Wireshark + C#으로 확인하는 PSH flag와 Nagle 알고리듬파일 다운로드1
12530정성태2/5/2021235개발 환경 구성: 536. Wireshark + C#으로 확인하는 TCP 통신의 Receive Window
12529정성태2/4/2021177개발 환경 구성: 535. Wireshark + C#으로 확인하는 TCP 통신의 MIN RTO
12528정성태2/9/2021205개발 환경 구성: 534. Wireshark + C#으로 확인하는 TCP 통신의 MSS(Maximum Segment Size) - 윈도우 환경
12527정성태2/1/2021284개발 환경 구성: 533. Wireshark + C#으로 확인하는 TCP 통신의 MSS(Maximum Segment Size) - 리눅스 환경파일 다운로드1
12526정성태2/1/2021158개발 환경 구성: 532. Azure Devops의 파이프라인 빌드 시 snk 파일 다루는 방법 - Secure file
12525정성태2/1/2021134개발 환경 구성: 531. Azure Devops - 파이프라인 실행 시 빌드 이벤트를 생략하는 방법
12524정성태2/18/2021197개발 환경 구성: 530. 기존 github 프로젝트를 Azure Devops의 빌드 Pipeline에 연결하는 방법 [1]
12523정성태1/31/2021172개발 환경 구성: 529. 기존 github 프로젝트를 Azure Devops의 Board에 연결하는 방법
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...