Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UDP hole punching implementation

Tags:

I am trying to accomplish UDP hole punching. I am basing my theory on this article and this WIKI page, but I am facing some issues with the C# coding of it. Here is my problem:

Using the code that was posted here I am now able to connect to a remote machine and listen on the same port for incoming connections (Bind 2 UDP clients to the same port).

For some reason the two bindings to the same port block each other from receiving any data. I have a UDP server that responds to my connection so if I connect to it first before binding any other client to the port I get its responses back.

If I bind another client to the port no data will be received on either clients.

Following are 2 code pieces that show my problem. The first connects to a remote server to create the rule on the NAT device and then a listener is started on a different thread to capture the incoming packets. The code then sends packets to the local IP so that the listener will get it. The second only sends packets to the local IP to make sure this works. I know this is not the actual hole punching as I am sending the packets to myself without living the NAT device at all. I am facing a problem at this point, and I don't imagine this will be any different if I use a computer out side the NAT device to connect.

[EDIT] 2/4/2012 I tried using another computer on my network and WireShark (packet sniffer) to test the listener. I see the packets incoming from the other computer but are not received by the listener UDP client (udpServer) or the sender UDP client (client).

[EDIT] 2/5/2010 I have now added a function call to close the first UDP client after the initial sending and receiving of packets only living the second UDP client to listen on the port. This works and I can receive packets from inside the network on that port. I will now try to send and receive packets from outside the network. I will post my findings as soon as I find something.

Using this code I get data on the listening client:

static void Main(string[] args) {     IPEndPoint localpt = new IPEndPoint(Dns.Resolve(Dns.GetHostName()).AddressList[0], 4545);      ThreadPool.QueueUserWorkItem(delegate     {         UdpClient udpServer = new UdpClient();         udpServer.ExclusiveAddressUse = false;         udpServer.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);         udpServer.Client.Bind(localpt);          IPEndPoint inEndPoint = new IPEndPoint(IPAddress.Any, 0);         Console.WriteLine("Listening on " + localpt + ".");         byte[] buffer = udpServer.Receive(ref inEndPoint); //this line will block forever         Console.WriteLine("Receive from " + inEndPoint + " " + Encoding.ASCII.GetString(buffer) + ".");     });      Thread.Sleep(1000);      UdpClient udpServer2 = new UdpClient(6000);      // the following lines work and the data is received     udpServer2.Connect(Dns.Resolve(Dns.GetHostName()).AddressList[0], 4545);     udpServer2.Send(new byte[] { 0x41 }, 1);      Console.Read(); } 

If I use the following code, after the connection and data transfer between my client and server, the listening UDP client will not receive anything:

static void Main(string[] args) {     IPEndPoint localpt = new IPEndPoint(Dns.Resolve(Dns.GetHostName()).AddressList[0], 4545);      //if the following lines up until serverConnect(); are removed all packets are received correctly     client = new UdpClient();     client.ExclusiveAddressUse = false;     client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);     client.Client.Bind(localpt);     remoteServerConnect(); //connection to remote server is done here                            //response is received correctly and printed to the console      ThreadPool.QueueUserWorkItem(delegate     {         UdpClient udpServer = new UdpClient();         udpServer.ExclusiveAddressUse = false;         udpServer.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);         udpServer.Client.Bind(localpt);          IPEndPoint inEndPoint = new IPEndPoint(IPAddress.Any, 0);         Console.WriteLine("Listening on " + localpt + ".");         byte[] buffer = udpServer.Receive(ref inEndPoint); //this line will block forever         Console.WriteLine("Receive from " + inEndPoint + " " + Encoding.ASCII.GetString(buffer) + ".");     });      Thread.Sleep(1000);      UdpClient udpServer2 = new UdpClient(6000);      // I expected the following line to work and to receive this as well     udpServer2.Connect(Dns.Resolve(Dns.GetHostName()).AddressList[0], 4545);     udpServer2.Send(new byte[] { 0x41 }, 1);      Console.Read(); } 
like image 983
brooc Avatar asked Feb 04 '12 11:02

brooc


People also ask

How UDP hole punching works?

UDP hole punching is one of the most common techniques used to establish UDP connections with systems behind NAT. It is called UDP hole punching because it punches a hole in the firewall of the network which allows a packet from an outside system to successfully reach the desired client on a network using NAT.

What is TCP UDP hole punching?

TCP hole punching is an experimentally used NAT traversal technique for establishing a TCP connection between two peers on the Internet behind NAT devices. NAT traversal is a general term for techniques that establish and maintain TCP/IP network and/or TCP connections traversing NAT gateways.

Does NAT work with UDP?

UDP hole punching is a commonly used technique employed in network address translation (NAT) applications for maintaining User Datagram Protocol (UDP) packet streams that traverse the NAT.


2 Answers

If i understand correctly, you are trying to communicate peer-to-peer between 2 clients each behind a different NAT, using a mediation server for hole punching?

Few years ago i did the exact same thing in c#, i haven't found the code yet, but ill give you some pointers if you like:

First, I wouldn't use the Connect() function on the udpclient, since UDP is a connectionless protocol, all this function really does is hide the functionality of a UDP socket.

You should perfrom the following steps:

  1. Open a UDP socket on a server with it's ports not blocked by a firewall, at a specific port (eg Bind this socket to a chosen port for example 23000)
  2. Create a UDP socket on the first client, and send something to the server at 23000. Do not bind this socket. When a udp is used to send a packet, windows will automatically assign a free port to the socket
  3. Do the same from the other client
  4. The server has now received 2 packets from 2 clients at 2 different adresses with 2 different ports. Test if the server can send packets back on the same address and port. (If this doesn't work you did something wrong or your NAT isn't working. You know its working if you can play games without opening ports :D)
  5. The server should now send the address and port of the other clients to each connected client.
  6. A client should now be able to send packets using UDP to the adresses received from the server.

You should note that the port used on the nat is probably not the same port as on your client pc!! The server should distribute this external port to clients. You must use the external adresses and the external ports to send to!

Also note that your NAT might not support this kind of port forwarding. Some NAT's forward all incoming traffic on a assigned port to you client, which is what you want. But some nats do filtering on the incoming packets adresses so it might block the other clients packets. This is unlikely though when using a standard personal user router.

like image 102
MHGameWork Avatar answered Oct 22 '22 01:10

MHGameWork


Edit: After a lot more testing this doesn't seem to work at all for me unless I enable UPnP. So a lot of the things I wrote here you may find useful but many people don't have UPnP enabled (because it is a security risk) so it will not work for them.

Here is some code using PubNub as a relay server :). I don't recommend using this code without testing because it is not perfect (I'm not sure if it is even secure or the right way to do things? idk I'm not a networking expert) but it should give you an idea of what to do. It at least has worked for me so far in a hobby project. The things it is missing are:

  • Testing if the client is on your LAN. I just send to both which works for your LAN and a device on another network but that is very inefficient.
  • Testing when the client stops listening if, for example, they closed the program. Because this is UDP it is stateless so it doesn't matter if we are sending messages into the void but we probably shouldn't do that if noone is getting them
  • I use Open.NAT to do port forwarding programatically but this might not work on some devices. Specifically, it uses UPnP which is a little insecure and requires UDP port 1900 to be port forwarded manually. Once they do this it is supported on most routers but many have not done this yet.

So first of all, you need a way to get your external and local IPs. Here is code for getting your local IP:

// From http://stackoverflow.com/questions/6803073/get-local-ip-address public string GetLocalIp() {     var host = Dns.GetHostEntry(Dns.GetHostName());     foreach (var ip in host.AddressList)     {         if (ip.AddressFamily == AddressFamily.InterNetwork)         {             return ip.ToString();         }     }     throw new Exception("Failed to get local IP"); } 

And here is some code for getting your external IP via trying a few websites that are designed to return your external IP

public string GetExternalIp() {     for (int i = 0; i < 2; i++)     {         string res = GetExternalIpWithTimeout(400);         if (res != "")         {             return res;         }     }     throw new Exception("Failed to get external IP"); } private static string GetExternalIpWithTimeout(int timeoutMillis) {     string[] sites = new string[] {       "http://ipinfo.io/ip",       "http://icanhazip.com/",       "http://ipof.in/txt",       "http://ifconfig.me/ip",       "http://ipecho.net/plain"     };     foreach (string site in sites)     {         try         {             HttpWebRequest request = (HttpWebRequest)WebRequest.Create(site);             request.Timeout = timeoutMillis;             using (var webResponse = (HttpWebResponse)request.GetResponse())             {                 using (Stream responseStream = webResponse.GetResponseStream())                 {                     using (StreamReader responseReader = new System.IO.StreamReader(responseStream, Encoding.UTF8))                     {                         return responseReader.ReadToEnd().Trim();                     }                 }             }         }         catch         {             continue;         }     }      return "";  } 

Now we need to find an open port and forward it to an external port. As mentioned above I used Open.NAT. First, you put together a list of ports that you think would be reasonable for your application to use after looking at registered UDP ports. Here are a few for example:

public static int[] ports = new int[] {   5283,   5284,   5285,   5286,   5287,   5288,   5289,   5290,   5291,   5292,   5293,   5294,   5295,   5296,   5297 }; 

Now we can loop through them and hopefully find one that is not in use to use port forwarding on:

public UdpClient GetUDPClientFromPorts(out Socket portHolder, out string localIp, out int localPort, out string externalIp, out int externalPort) {   localIp = GetLocalIp();   externalIp = GetExternalIp();    var discoverer = new Open.Nat.NatDiscoverer();   var device = discoverer.DiscoverDeviceAsync().Result;    IPAddress localAddr = IPAddress.Parse(localIp);   int workingPort = -1;   for (int i = 0; i < ports.Length; i++)   {       try       {           // You can alternatively test tcp with  nc -vz externalip 5293 in linux and           // udp with  nc -vz -u externalip 5293 in linux           Socket tempServer = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);           tempServer.Bind(new IPEndPoint(localAddr, ports[i]));           tempServer.Close();           workingPort = ports[i];           break;       }       catch       {         // Binding failed, port is in use, try next one       }   }     if (workingPort == -1)   {       throw new Exception("Failed to connect to a port");   }     int localPort = workingPort;    // You could try a different external port if the below code doesn't work   externalPort = workingPort;    // Mapping ports   device.CreatePortMapAsync(new Open.Nat.Mapping(Open.Nat.Protocol.Udp, localPort, externalPort));    // Bind a socket to our port to "claim" it or cry if someone else is now using it   try   {       portHolder = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);       portHolder.Bind(new IPEndPoint(localAddr, localPort));   }   catch   {       throw new Exception("Failed, someone is now using local port: " + localPort);   }     // Make a UDP Client that will use that port   UdpClient udpClient = new UdpClient(localPort);   return udpClient; } 

Now for the PubNub relay server code (P2PPeer will be defined later below). There is a lot here so I'm not really gonna explain it but hopefully the code is clear enough to help you understand what is going on

public delegate void NewPeerCallback(P2PPeer newPeer); public event NewPeerCallback OnNewPeerConnection;  public Pubnub pubnub; public string pubnubChannelName; public string localIp; public string externalIp; public int localPort; public int externalPort; public UdpClient udpClient; HashSet<string> uniqueIdsPubNubSeen; object peerLock = new object(); Dictionary<string, P2PPeer> connectedPeers; string myPeerDataString;  public void InitPubnub(string pubnubPublishKey, string pubnubSubscribeKey, string pubnubChannelName) {     uniqueIdsPubNubSeen = new HashSet<string>();     connectedPeers = new Dictionary<string, P2PPeer>;     pubnub = new Pubnub(pubnubPublishKey, pubnubSubscribeKey);     myPeerDataString = localIp + " " + externalIp + " " + localPort + " " + externalPort + " " + pubnub.SessionUUID;     this.pubnubChannelName = pubnubChannelName;     pubnub.Subscribe<string>(         pubnubChannelName,         OnPubNubMessage,         OnPubNubConnect,         OnPubNubError);     return pubnub; }  //// Subscribe callbacks void OnPubNubConnect(string res) {     pubnub.Publish<string>(pubnubChannelName, connectionDataString, OnPubNubTheyGotMessage, OnPubNubMessageFailed); }  void OnPubNubError(PubnubClientError clientError) {     throw new Exception("PubNub error on subscribe: " + clientError.Message); }  void OnPubNubMessage(string message) {     // The message will be the string ["localIp externalIp localPort externalPort","messageId","channelName"]     string[] splitMessage = message.Trim().Substring(1, message.Length - 2).Split(new char[] { ',' });     string peerDataString = splitMessage[0].Trim().Substring(1, splitMessage[0].Trim().Length - 2);      // If you want these, I don't need them     //string peerMessageId = splitMessage[1].Trim().Substring(1, splitMessage[1].Trim().Length - 2);     //string channelName = splitMessage[2].Trim().Substring(1, splitMessage[2].Trim().Length - 2);       string[] pieces = peerDataString.Split(new char[] { ' ', '\t' });     string peerLocalIp = pieces[0].Trim();     string peerExternalIp = pieces[1].Trim();     string peerLocalPort = int.Parse(pieces[2].Trim());     string peerExternalPort = int.Parse(pieces[3].Trim());     string peerPubnubUniqueId = pieces[4].Trim();      pubNubUniqueId = pieces[4].Trim();      // If you are on the same device then you have to do this for it to work idk why     if (peerLocalIp == localIp && peerExternalIp == externalIp)     {         peerLocalIp = "127.0.0.1";     }       // From me, ignore     if (peerPubnubUniqueId == pubnub.SessionUUID)     {         return;     }      // We haven't set up our connection yet, what are we doing     if (udpClient == null)     {         return;     }       // From someone else       IPEndPoint peerEndPoint = new IPEndPoint(IPAddress.Parse(peerExternalIp), peerExternalPort);     IPEndPoint peerEndPointLocal = new IPEndPoint(IPAddress.Parse(peerLocalIp), peerLocalPort);      // First time we have heard from them     if (!uniqueIdsPubNubSeen.Contains(peerPubnubUniqueId))     {         uniqueIdsPubNubSeen.Add(peerPubnubUniqueId);          // Dummy messages to do UDP hole punching, these may or may not go through and that is fine         udpClient.Send(new byte[10], 10, peerEndPoint);         udpClient.Send(new byte[10], 10, peerEndPointLocal); // This is if they are on a LAN, we will try both         pubnub.Publish<string>(pubnubChannelName, myPeerDataString, OnPubNubTheyGotMessage, OnPubNubMessageFailed);     }     // Second time we have heard from them, after then we don't care because we are connected     else if (!connectedPeers.ContainsKey(peerPubnubUniqueId))     {         //bool isOnLan = IsOnLan(IPAddress.Parse(peerExternalIp)); TODO, this would be nice to test for         bool isOnLan = false; // For now we will just do things for both         P2PPeer peer = new P2PPeer(peerLocalIp, peerExternalIp, peerLocalPort, peerExternalPort, this, isOnLan);         lock (peerLock)         {             connectedPeers.Add(peerPubnubUniqueId, peer);         }          // More dummy messages because why not         udpClient.Send(new byte[10], 10, peerEndPoint);         udpClient.Send(new byte[10], 10, peerEndPointLocal);           pubnub.Publish<string>(pubnubChannelName, connectionDataString, OnPubNubTheyGotMessage, OnPubNubMessageFailed);         if (OnNewPeerConnection != null)         {             OnNewPeerConnection(peer);         }     } }  //// Publish callbacks void OnPubNubTheyGotMessage(object result) {  }  void OnPubNubMessageFailed(PubnubClientError clientError) {     throw new Exception("PubNub error on publish: " + clientError.Message); } 

And here is a P2PPeer

public class P2PPeer {     public string localIp;     public string externalIp;     public int localPort;     public int externalPort;     public bool isOnLan;      P2PClient client;      public delegate void ReceivedBytesFromPeerCallback(byte[] bytes);      public event ReceivedBytesFromPeerCallback OnReceivedBytesFromPeer;       public P2PPeer(string localIp, string externalIp, int localPort, int externalPort, P2PClient client, bool isOnLan)     {         this.localIp = localIp;         this.externalIp = externalIp;         this.localPort = localPort;         this.externalPort = externalPort;         this.client = client;         this.isOnLan = isOnLan;            if (isOnLan)         {             IPEndPoint endPointLocal = new IPEndPoint(IPAddress.Parse(localIp), localPort);             Thread localListener = new Thread(() => ReceiveMessage(endPointLocal));             localListener.IsBackground = true;             localListener.Start();         }          else         {             IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(externalIp), externalPort);             Thread externalListener = new Thread(() => ReceiveMessage(endPoint));             externalListener.IsBackground = true;             externalListener.Start();         }     }      public void SendBytes(byte[] data)     {         if (client.udpClient == null)         {             throw new Exception("P2PClient doesn't have a udpSocket open anymore");         }         //if (isOnLan) // This would work but I'm not sure how to test if they are on LAN so I'll just use both for now         {             client.udpClient.Send(data, data.Length, new IPEndPoint(IPAddress.Parse(localIp), localPort));         }         //else         {             client.udpClient.Send(data, data.Length, new IPEndPoint(IPAddress.Parse(externalIp), externalPort));         }     }      // Encoded in UTF8     public void SendString(string str)     {         SendBytes(System.Text.Encoding.UTF8.GetBytes(str));     }       void ReceiveMessage(IPEndPoint endPoint)     {         while (client.udpClient != null)         {             byte[] message = client.udpClient.Receive(ref endPoint);             if (OnReceivedBytesFromPeer != null)             {                 OnReceivedBytesFromPeer(message);             }             //string receiveString = Encoding.UTF8.GetString(message);             //Console.Log("got: " + receiveString);         }     } } 

Finally, here are all my usings:

using PubNubMessaging.Core; // Get from PubNub GitHub for C#, I used the Unity3D library using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; 

I'm open to comments and questions, feel free to give feedback if something here is bad practice or doesn't work. A few bugs were introduced in translation from my code that I'll fix here eventually but this should at least give you the idea of what to do.

like image 22
Phylliida Avatar answered Oct 22 '22 01:10

Phylliida