UDP hole punching implementation

后端 未结 5 1516
伪装坚强ぢ
伪装坚强ぢ 2020-12-02 07:11

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 probl

5条回答
  •  孤街浪徒
    2020-12-02 07:34

    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 uniqueIdsPubNubSeen;
    object peerLock = new object();
    Dictionary connectedPeers;
    string myPeerDataString;
    
    public void InitPubnub(string pubnubPublishKey, string pubnubSubscribeKey, string pubnubChannelName)
    {
        uniqueIdsPubNubSeen = new HashSet();
        connectedPeers = new Dictionary;
        pubnub = new Pubnub(pubnubPublishKey, pubnubSubscribeKey);
        myPeerDataString = localIp + " " + externalIp + " " + localPort + " " + externalPort + " " + pubnub.SessionUUID;
        this.pubnubChannelName = pubnubChannelName;
        pubnub.Subscribe(
            pubnubChannelName,
            OnPubNubMessage,
            OnPubNubConnect,
            OnPubNubError);
        return pubnub;
    }
    
    //// Subscribe callbacks
    void OnPubNubConnect(string res)
    {
        pubnub.Publish(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(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(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.

提交回复
热议问题