C# UDP打孔实现

C# UDP打孔实现,c#,udp,traversal,nat,hole-punching,C#,Udp,Traversal,Nat,Hole Punching,我正在努力完成UDP打孔。我的理论是基于这一点的,但我在对其进行C#编码时遇到了一些问题。我的问题是: 使用发布的代码,我现在可以连接到远程机器,并在同一端口上侦听传入连接(将2个UDP客户端绑定到同一端口) 出于某种原因,同一端口的两个绑定会阻止对方接收任何数据。 我有一个UDP服务器响应我的连接,所以如果我在将任何其他客户端绑定到端口之前先连接到它,我会得到它的响应 如果我将另一个客户端绑定到该端口,则两个客户端上都不会收到任何数据 下面是显示我的问题的两段代码。首先连接到远程服务器以在NA

我正在努力完成UDP打孔。我的理论是基于这一点的,但我在对其进行C#编码时遇到了一些问题。我的问题是:

使用发布的代码,我现在可以连接到远程机器,并在同一端口上侦听传入连接(将2个UDP客户端绑定到同一端口)

出于某种原因,同一端口的两个绑定会阻止对方接收任何数据。 我有一个UDP服务器响应我的连接,所以如果我在将任何其他客户端绑定到端口之前先连接到它,我会得到它的响应

如果我将另一个客户端绑定到该端口,则两个客户端上都不会收到任何数据

下面是显示我的问题的两段代码。首先连接到远程服务器以在NAT设备上创建规则,然后在不同的线程上启动侦听器以捕获传入的数据包。然后,代码将数据包发送到本地IP,以便侦听器将其获取。第二个只向本地IP发送数据包以确保其正常工作。我知道这并不是真正的打孔,因为我根本没有使用NAT设备,而是将数据包发送给自己。在这一点上我面临着一个问题,我不认为如果我在NAT设备外使用一台计算机进行连接会有什么不同

[编辑]2012年2月4日 我尝试使用网络上的另一台计算机和WireShark(数据包嗅探器)来测试侦听器。我看到从另一台计算机传入的数据包,但侦听器UDP客户端(udpServer)或发送器UDP客户端(客户端)未接收到这些数据包

[编辑]2010年2月5日
我现在添加了一个函数调用,在最初发送和接收数据包之后关闭第一个UDP客户端,只有第二个UDP客户端在端口上侦听数据包。这是可行的,我可以在该端口上从网络内部接收数据包。我现在将尝试从网络外部发送和接收数据包。一旦我发现了什么,我会马上发布我的发现

使用此代码,我可以获取侦听客户端上的数据:

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();
}
如果使用以下代码,在客户端和服务器之间进行连接和数据传输后,侦听UDP客户端将不会接收任何内容:

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();
}

更新:

首先绑定的UDPClient中的任何一个都是将由Windows发送到传入数据包的UDPClient。在您的示例中,尝试将设置侦听线程的代码块移动到顶部

您确定问题不仅仅在于接收线程只被写入处理单个接收吗?尝试将接收线程替换为,如下所示

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 + ".");

    while (inEndPoint != null)
    {
        byte[] buffer = udpServer.Receive(ref inEndPoint);
        Console.WriteLine("Bytes received from " + inEndPoint + " " + Encoding.ASCII.GetString(buffer) + ".");
    }
});

您是否尝试过使用异步函数,下面是一个如何使其工作的示例—可能需要一些工作才能使其100%正常工作:

    public void HolePunch(String ServerIp, Int32 Port)
    {
        IPEndPoint LocalPt = new IPEndPoint(Dns.GetHostEntry(Dns.GetHostName()).AddressList[0], Port);
        UdpClient Client = new UdpClient();
        Client.ExclusiveAddressUse = false;
        Client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        Client.Client.Bind(LocalPt);

        IPEndPoint RemotePt = new IPEndPoint(IPAddress.Parse(ServerIp), Port);

        // This Part Sends your local endpoint to the server so if the two peers are on the same nat they can bypass it, you can omit this if you wish to just use the remote endpoint.
        byte[] IPBuffer = System.Text.Encoding.UTF8.GetBytes(Dns.GetHostEntry(Dns.GetHostName()).AddressList[0].ToString());
        byte[] LengthBuffer = BitConverter.GetBytes(IPBuffer.Length);
        byte[] PortBuffer = BitConverter.GetBytes(Port);
        byte[] Buffer = new byte[IPBuffer.Length + LengthBuffer.Length + PortBuffer.Length];
        LengthBuffer.CopyTo(Buffer,0);
        IPBuffer.CopyTo(Buffer, LengthBuffer.Length);
        PortBuffer.CopyTo(Buffer, IPBuffer.Length + LengthBuffer.Length);
        Client.BeginSend(Buffer, Buffer.Length, RemotePt, new AsyncCallback(SendCallback), Client);

        // Wait to receve something
        BeginReceive(Client, Port);

        // you may want to use a auto or manual ResetEvent here and have the server send back a confirmation, the server should have now stored your local (you sent it) and remote endpoint.

        // you now need to work out who you need to connect to then ask the server for there remote and local end point then need to try to connect to the local first then the remote.
        // if the server knows who you need to connect to you could just have it send you the endpoints as the confirmation.

        // you may also need to keep this open with a keepalive packet untill it is time to connect to the peer or peers.

        // once you have the endpoints of the peer you can close this connection unless you need to keep asking the server for other endpoints

        Client.Close();
    }

    public void ConnectToPeer(String PeerIp, Int32 Port)
    {
        IPEndPoint LocalPt = new IPEndPoint(Dns.GetHostEntry(Dns.GetHostName()).AddressList[0], Port);
        UdpClient Client = new UdpClient();
        Client.ExclusiveAddressUse = false;
        Client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        Client.Client.Bind(LocalPt);
        IPEndPoint RemotePt = new IPEndPoint(IPAddress.Parse(PeerIp), Port);
        Client.Connect(RemotePt);
        //you may want to keep the peer client connections in a list.

        BeginReceive(Client, Port);
    }

    public void SendCallback(IAsyncResult ar)
    {
        UdpClient Client = (UdpClient)ar.AsyncState;
        Client.EndSend(ar);
    }

    public void BeginReceive(UdpClient Client, Int32 Port)
    {
        IPEndPoint ListenPt = new IPEndPoint(IPAddress.Any, Port);

        Object[] State = new Object[] { Client, ListenPt };

        Client.BeginReceive(new AsyncCallback(ReceiveCallback), State);
    }

    public void ReceiveCallback(IAsyncResult ar)
    {
        UdpClient Client = (UdpClient)((Object[])ar.AsyncState)[0];
        IPEndPoint ListenPt = (IPEndPoint)((Object[])ar.AsyncState)[0];

        Byte[] receiveBytes = Client.EndReceive(ar, ref ListenPt);
    }

我希望这能有所帮助。

如果我理解正确,您正在尝试使用中介服务器在不同NAT后面的两个客户端之间进行点对点通信

几年前,我在c#中做了完全相同的事情,我还没有找到代码,但如果您愿意,我会给您一些提示:

首先,我不会在udpclient上使用Connect()函数,因为UDP是一种无连接协议,所以这个函数真正做的就是隐藏UDP套接字的功能

您应该执行以下步骤:

  • 打开服务器上的UDP套接字,其端口未被防火墙阻止,位于特定端口(例如将该套接字绑定到所选端口,例如23000)
  • 在第一个客户端上创建一个UDP套接字,并在23000向服务器发送内容不要绑定此套接字。当使用udp发送数据包时,windows将自动为套接字分配一个空闲端口
  • 从另一个客户端执行同样的操作
  • 服务器现在已从2个客户端接收到2个数据包,它们位于2个不同的地址和2个不同的端口。测试服务器是否可以将数据包发送回相同的地址和端口。(如果这不起作用,您做错了什么,或者您的NAT不起作用。如果您可以在不打开端口的情况下玩游戏,您就知道它起作用了:D)
  • 服务器现在应该将其他客户端的地址和端口发送到每个连接的客户端
  • 客户端现在应该能够使用UDP将数据包发送到从服务器接收的地址
  • 您应该注意,nat上使用的端口可能与客户端pc上的端口不同!!服务器应将此外部端口分发给客户端您必须使用外部地址和外部端口发送到


    还请注意,您的NAT可能不支持这种端口转发。一些NAT将分配端口上的所有传入流量转发给您的客户端,这正是您所需要的。但是有些NAT会对传入的数据包地址进行过滤,因此可能会阻止其他客户端的数据包。这在使用标准个人用户路由器时是不太可能的。编辑:经过大量测试后,除非启用UPnP,否则这对我来说似乎根本不起作用。所以我在这里写的很多东西你们可能会发现很有用,但很多人并没有启用UPnP(因为这是一个安全风险),所以它对他们不起作用

    下面是一些将PubNub用作中继服务器的代码:)。我不建议在未经测试的情况下使用此代码,因为它并不完美(我不确定它是否安全或是否是正确的操作方式?idk我不是网络专家),但它应该让您知道该怎么做。至少到目前为止,它在我的一个爱好项目中起到了作用。它缺少的是:

    • 测试客户端是否在您的局域网上。我只是发送到这两个工程为您的局域网和另一个网络上的设备,但这是非常低效的
    • 测试客户端何时停止侦听,例如,如果客户端关闭了程序。因为这是UDP,它是无状态的,所以我们是否将消息发送到void中并不重要,但是如果没有人接收到消息,我们可能不应该这样做
    • 我使用编程方式进行端口转发,但这在某些de上可能不起作用
      // 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");
      }
      
      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 "";
      
      }
      
      public static int[] ports = new int[]
      {
        5283,
        5284,
        5285,
        5286,
        5287,
        5288,
        5289,
        5290,
        5291,
        5292,
        5293,
        5294,
        5295,
        5296,
        5297
      };
      
      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;
      }
      
      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);
      }
      
      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);
              }
          }
      }
      
      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;
      
      using System;
      using System.Collections.Generic;
      using System.Net;
      using System.Net.Sockets;
      using HolePunching.Common;
      
      namespace HolePunching.Server
      {
          class Server
          {
              private static bool _isRunning;
              private static UdpClient _udpClient;
              private static readonly Dictionary<byte, PeerContext> Contexts = new Dictionary<byte, PeerContext>();
      
              private static readonly Dictionary<byte, byte> Mappings = new Dictionary<byte, byte>
              {
                  {1, 2},
                  {2, 1},
              };
      
              static void Main()
              {
                  _udpClient = new UdpClient( Consts.UdpPort );
                  ListenUdp();
      
                  Console.ReadLine();
                  _isRunning = false;
              }
      
              private static async void ListenUdp()
              {
                  _isRunning = true;
      
                  while ( _isRunning )
                  {
                      try
                      {
                          var receivedResults = await _udpClient.ReceiveAsync();
      
                          if ( !_isRunning )
                          {
                              break;
                          }
      
                          ProcessUdpMessage( receivedResults.Buffer, receivedResults.RemoteEndPoint );
                      }
                      catch ( Exception ex )
                      {
                          Console.WriteLine( $"Error: {ex.Message}" );
                      }
                  }
              }
      
              private static void ProcessUdpMessage( byte[] buffer, IPEndPoint remoteEndPoint )
              {
                  if ( !UdpProtocol.UdpInfoMessage.TryParse( buffer, out UdpProtocol.UdpInfoMessage message ) )
                  {
                      Console.WriteLine( $" >>> Got shitty UDP [ {remoteEndPoint.Address} : {remoteEndPoint.Port} ]" );
                      _udpClient.Send( new byte[] { 1 }, 1, remoteEndPoint );
                      return;
                  }
      
                  Console.WriteLine( $" >>> Got UDP from {message.Id}. [ {remoteEndPoint.Address} : {remoteEndPoint.Port} ]" );
      
                  if ( !Contexts.TryGetValue( message.Id, out PeerContext context ) )
                  {
                      context = new PeerContext
                      {
                          PeerId = message.Id,
                          PublicUdpEndPoint = remoteEndPoint,
                          LocalUdpEndPoint = new IPEndPoint( message.LocalIp, message.LocalPort ),
                      };
      
                      Contexts.Add( context.PeerId, context );
                  }
      
                  byte partnerId = Mappings[context.PeerId];
                  if ( !Contexts.TryGetValue( partnerId, out context ) )
                  {
                      _udpClient.Send( new byte[] { 1 }, 1, remoteEndPoint );
                      return;
                  }
      
                  var response = UdpProtocol.PeerAddressMessage.GetMessage(
                      partnerId,
                      context.PublicUdpEndPoint.Address,
                      context.PublicUdpEndPoint.Port,
                      context.LocalUdpEndPoint.Address,
                      context.LocalUdpEndPoint.Port );
      
                  _udpClient.Send( response.Data, response.Data.Length, remoteEndPoint );
      
                  Console.WriteLine( $" <<< Responsed to {message.Id}" );
              }
          }
      
          public class PeerContext
          {
              public byte PeerId { get; set; }
              public IPEndPoint PublicUdpEndPoint { get; set; }
              public IPEndPoint LocalUdpEndPoint { get; set; }
          }
      }
      
      using System;
      
      namespace HolePunching.Client
      {
          class Client
          {
              public const string ServerIp = "your.server.public.address";
      
              static void Main()
              {
                  byte id = ReadIdFromConsole();
      
                  // you need some smarter :)
                  int localPort = id == 1 ? 61043 : 59912;
                  var x = new Demo( ServerIp, id, localPort );
                  x.Start();
              }
      
              private static byte ReadIdFromConsole()
              {
                  Console.Write( "Peer id (1 or 2): " );
      
                  var id = byte.Parse( Console.ReadLine() );
      
                  Console.Title = $"Peer {id}";
      
                  return id;
              }
          }
      }
      
      using HolePunching.Common;
      using System;
      using System.Collections.Generic;
      using System.Net;
      using System.Net.Sockets;
      using System.Threading;
      using System.Threading.Tasks;
      
      namespace HolePunching.Client
      {
          public class Demo
          {
              private static bool _isRunning;
      
              private static UdpClient _udpPuncher;
              private static UdpClient _udpClient;
              private static UdpClient _extraUdpClient;
              private static bool _extraUdpClientConnected;
      
              private static byte _id;
      
              private static IPEndPoint _localEndPoint;
              private static IPEndPoint _serverUdpEndPoint;
              private static IPEndPoint _partnerPublicUdpEndPoint;
              private static IPEndPoint _partnerLocalUdpEndPoint;
      
              private static 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" );
              }
      
              public Demo( string serverIp, byte id, int localPort )
              {
                  _serverUdpEndPoint = new IPEndPoint( IPAddress.Parse( serverIp ), Consts.UdpPort );
                  _id = id;
      
                  // we have to bind all our UdpClients to this endpoint
                  _localEndPoint = new IPEndPoint( IPAddress.Parse( GetLocalIp() ), localPort );
              }
      
              public void Start(  )
              {
                  _udpPuncher = new UdpClient(); // this guy is just for punching
                  _udpClient = new UdpClient(); // this will keep hole alive, and also can send data
                  _extraUdpClient = new UdpClient(); // i think, this guy is the best option for sending data (explained below)
      
                  InitUdpClients( new[] { _udpPuncher, _udpClient, _extraUdpClient }, _localEndPoint );
      
                  Task.Run( (Action) SendUdpMessages );
                  Task.Run( (Action) ListenUdp );
      
                  Console.ReadLine();
                  _isRunning = false;
              }
      
              private void InitUdpClients(IEnumerable<UdpClient> clients, EndPoint localEndPoint)
              {
                  // if you don't want to use explicit localPort, you should create here one more UdpClient (X) and send something to server (it will automatically bind X to free port). then bind all clients to this port and close X
      
                  foreach ( var udpClient in clients )
                  {
                      udpClient.ExclusiveAddressUse = false;
                      udpClient.Client.SetSocketOption( SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true );
                      udpClient.Client.Bind( localEndPoint );
                  }
              }
      
              private void SendUdpMessages()
              {
                  _isRunning = true;
      
                  var messageToServer = UdpProtocol.UdpInfoMessage.GetMessage( _id, _localEndPoint.Address, _localEndPoint.Port );
                  var messageToPeer = UdpProtocol.P2PKeepAliveMessage.GetMessage();
      
                  while ( _isRunning )
                  {
                      // while we dont have partner's address, we will send messages to server
                      if ( _partnerPublicUdpEndPoint == null && _partnerLocalUdpEndPoint == null )
                      {
                          _udpPuncher.Send( messageToServer.Data, messageToServer.Data.Length, _serverUdpEndPoint );
                          Console.WriteLine( $" >>> Sent UDP to server [ {_serverUdpEndPoint.Address} : {_serverUdpEndPoint.Port} ]" );
                      }
                      else
                      {
                          // you can skip it. just demonstration, that you still can send messages to server
                          _udpClient.Send( messageToServer.Data, messageToServer.Data.Length, _serverUdpEndPoint );
                          Console.WriteLine( $" >>> Sent UDP to server [ {_serverUdpEndPoint.Address} : {_serverUdpEndPoint.Port} ]" );
      
                          // THIS is how we punching hole! very first this message should be dropped by partner's NAT, but it's ok.
                          // i suppose that this is good idea to send this "keep-alive" messages to peer even if you are connected already,
                          // because AFAIK "hole" for UDP lives ~2 minutes on NAT. so "we will let it die? NEVER!" (c)
                          _udpClient.Send( messageToPeer.Data, messageToPeer.Data.Length, _partnerPublicUdpEndPoint );
                          _udpClient.Send( messageToPeer.Data, messageToPeer.Data.Length, _partnerLocalUdpEndPoint );
                          Console.WriteLine( $" >>> Sent UDP to peer.public [ {_partnerPublicUdpEndPoint.Address} : {_partnerPublicUdpEndPoint.Port} ]" );
                          Console.WriteLine( $" >>> Sent UDP to peer.local [ {_partnerLocalUdpEndPoint.Address} : {_partnerLocalUdpEndPoint.Port} ]" );
      
                          // "connected" UdpClient sends data much faster, 
                          // so if you have something that your partner cant wait for (voice, for example), send it this way
                          if ( _extraUdpClientConnected )
                          {
                              _extraUdpClient.Send( messageToPeer.Data, messageToPeer.Data.Length );
                              Console.WriteLine( $" >>> Sent UDP to peer.received EP" );
                          }
                      }
      
                      Thread.Sleep( 3000 );
                  }
              }
      
              private async void ListenUdp()
              {
                  _isRunning = true;
      
                  while ( _isRunning )
                  {
                      try
                      {
                          // also important thing!
                          // when you did not punched hole yet, you must listen incoming packets using "puncher" (later we will close it).
                          // where you already have p2p connection (and "puncher" closed), use "non-puncher"
                          UdpClient udpClient = _partnerPublicUdpEndPoint == null ? _udpPuncher : _udpClient;
      
                          var receivedResults = await udpClient.ReceiveAsync();
      
                          if ( !_isRunning )
                          {
                              break;
                          }
      
                          ProcessUdpMessage( receivedResults.Buffer, receivedResults.RemoteEndPoint );
                      }
                      catch ( SocketException ex )
                      {
                          // do something here...
                      }
                      catch ( Exception ex )
                      {
                          Console.WriteLine( $"Error: {ex.Message}" );
                      }
                  }
              }
      
              private static void ProcessUdpMessage( byte[] buffer, IPEndPoint remoteEndPoint )
              {
                  // if server sent partner's endpoinps, we will store it and (IMPORTANT) close "puncher"
                  if ( UdpProtocol.PeerAddressMessage.TryParse( buffer, out UdpProtocol.PeerAddressMessage peerAddressMessage ) )
                  {
                      Console.WriteLine( " <<< Got response from server" );
                      _partnerPublicUdpEndPoint = new IPEndPoint( peerAddressMessage.PublicIp, peerAddressMessage.PublicPort );
                      _partnerLocalUdpEndPoint = new IPEndPoint( peerAddressMessage.LocalIp, peerAddressMessage.LocalPort );
      
                      _udpPuncher.Close();
                  }
                  // since we got this message we know partner's endpoint for sure, 
                  // and we can "connect" UdpClient to it, so it will work faster
                  else if ( UdpProtocol.P2PKeepAliveMessage.TryParse( buffer ) )
                  {
                      Console.WriteLine( $"           IT WORKS!!! WOW!!!  [ {remoteEndPoint.Address} : {remoteEndPoint.Port} ]" );
      
                      _extraUdpClientConnected = true;
                      _extraUdpClient.Connect( remoteEndPoint );
                  }
                  else
                  {
                      Console.WriteLine( "???" );
                  }
              }
          }
      }
      
      using System;
      using System.Linq;
      using System.Net;
      using System.Text;
      
      namespace HolePunching.Common
      {
          public static class UdpProtocol
          {
              public static readonly int GuidLength = 16;
              public static readonly int PeerIdLength = 1;
              public static readonly int IpLength = 4;
              public static readonly int IntLength = 4;
      
              public static readonly byte[] Prefix = { 12, 23, 34, 45 };
      
              private static byte[] JoinBytes( params byte[][] bytes )
              {
                  var result = new byte[bytes.Sum( x => x.Length )];
                  int pos = 0;
      
                  for ( int i = 0; i < bytes.Length; i++ )
                  {
                      for ( int j = 0; j < bytes[i].Length; j++, pos++ )
                      {
                          result[pos] = bytes[i][j];
                      }
                  }
      
                  return result;
              }
      
              #region Helper extensions
      
              private static bool StartsWith( this byte[] @this, byte[] value, int offset = 0 )
              {
                  if ( @this == null || value == null || @this.Length < offset + value.Length )
                  {
                      return false;
                  }
      
                  for ( int i = 0; i < value.Length; i++ )
                  {
                      if ( @this[i + offset] < value[i] )
                      {
                          return false;
                      }
                  }
      
                  return true;
              }
      
              private static byte[] ToUnicodeBytes( this string @this )
              {
                  return Encoding.Unicode.GetBytes( @this );
              }
      
              private static byte[] Take( this byte[] @this, int offset, int length )
              {
                  return @this.Skip( offset ).Take( length ).ToArray();
              }
      
              public static bool IsSuitableUdpMessage( this byte[] @this )
              {
                  return @this.StartsWith( Prefix );
              }
      
              public static int GetInt( this byte[] @this )
              {
                  if ( @this.Length != 4 )
                      throw new ArgumentException( "Byte array must be exactly 4 bytes to be convertible to uint." );
      
                  return ( ( ( @this[0] << 8 ) + @this[1] << 8 ) + @this[2] << 8 ) + @this[3];
              }
      
              public static byte[] ToByteArray( this int value )
              {
                  return new[]
                  {
                      (byte)(value >> 24),
                      (byte)(value >> 16),
                      (byte)(value >> 8),
                      (byte)value
                  };
              }
      
              #endregion
      
              #region Messages
      
              public abstract class UdpMessage
              {
                  public byte[] Data { get; }
      
                  protected UdpMessage( byte[] data )
                  {
                      Data = data;
                  }
              }
      
              public class UdpInfoMessage : UdpMessage
              {
                  private static readonly byte[] MessagePrefix = { 41, 57 };
                  private static readonly int MessageLength = Prefix.Length + MessagePrefix.Length + PeerIdLength + IpLength + IntLength;
      
                  public byte Id { get; }
                  public IPAddress LocalIp { get; }
                  public int LocalPort { get; }
      
                  private UdpInfoMessage( byte[] data, byte id, IPAddress localIp, int localPort )
                      : base( data )
                  {
                      Id = id;
                      LocalIp = localIp;
                      LocalPort = localPort;
                  }
      
                  public static UdpInfoMessage GetMessage( byte id, IPAddress localIp, int localPort )
                  {
                      var data = JoinBytes( Prefix, MessagePrefix, new[] { id }, localIp.GetAddressBytes(), localPort.ToByteArray() );
      
                      return new UdpInfoMessage( data, id, localIp, localPort );
                  }
      
                  public static bool TryParse( byte[] data, out UdpInfoMessage message )
                  {
                      message = null;
      
                      if ( !data.StartsWith( Prefix ) )
                          return false;
                      if ( !data.StartsWith( MessagePrefix, Prefix.Length ) )
                          return false;
                      if ( data.Length != MessageLength )
                          return false;
      
                      int index = Prefix.Length + MessagePrefix.Length;
                      byte id = data[index];
      
                      index += PeerIdLength;
                      byte[] localIpBytes = data.Take( index, IpLength );
                      var localIp = new IPAddress( localIpBytes );
      
                      index += IpLength;
                      byte[] localPortBytes = data.Take( index, IntLength );
                      int localPort = localPortBytes.GetInt();
      
                      message = new UdpInfoMessage( data, id, localIp, localPort );
      
                      return true;
                  }
              }
      
              public class PeerAddressMessage : UdpMessage
              {
                  private static readonly byte[] MessagePrefix = { 36, 49 };
                  private static readonly int MessageLength = Prefix.Length + MessagePrefix.Length + PeerIdLength + ( IpLength + IntLength ) * 2;
      
                  public byte Id { get; }
                  public IPAddress PublicIp { get; }
                  public int PublicPort { get; }
                  public IPAddress LocalIp { get; }
                  public int LocalPort { get; }
      
                  private PeerAddressMessage( byte[] data, byte id, IPAddress publicIp, int publicPort, IPAddress localIp, int localPort )
                      : base( data )
                  {
                      Id = id;
                      PublicIp = publicIp;
                      PublicPort = publicPort;
                      LocalIp = localIp;
                      LocalPort = localPort;
                  }
      
                  public static PeerAddressMessage GetMessage( byte id, IPAddress publicIp, int publicPort, IPAddress localIp, int localPort )
                  {
                      var data = JoinBytes( Prefix, MessagePrefix, new[] { id }, 
                          publicIp.GetAddressBytes(), publicPort.ToByteArray(),
                          localIp.GetAddressBytes(), localPort.ToByteArray() );
      
                      return new PeerAddressMessage( data, id, publicIp, publicPort, localIp, localPort );
                  }
      
                  public static bool TryParse( byte[] data, out PeerAddressMessage message )
                  {
                      message = null;
      
                      if ( !data.StartsWith( Prefix ) )
                          return false;
                      if ( !data.StartsWith( MessagePrefix, Prefix.Length ) )
                          return false;
                      if ( data.Length != MessageLength )
                          return false;
      
                      int index = Prefix.Length + MessagePrefix.Length;
                      byte id = data[index];
      
                      index += PeerIdLength;
                      byte[] publicIpBytes = data.Take( index, IpLength );
                      var publicIp = new IPAddress( publicIpBytes );
      
                      index += IpLength;
                      byte[] publicPortBytes = data.Take( index, IntLength );
                      int publicPort = publicPortBytes.GetInt();
      
                      index += IntLength;
                      byte[] localIpBytes = data.Take( index, IpLength );
                      var localIp = new IPAddress( localIpBytes );
      
                      index += IpLength;
                      byte[] localPortBytes = data.Take( index, IntLength );
                      int localPort = localPortBytes.GetInt();
      
                      message = new PeerAddressMessage( data, id, publicIp, publicPort, localIp, localPort );
      
                      return true;
                  }
              }
      
              public class P2PKeepAliveMessage : UdpMessage
              {
                  private static readonly byte[] MessagePrefix = { 11, 19 };
                  private static P2PKeepAliveMessage _message;
      
                  private P2PKeepAliveMessage( byte[] data )
                      : base( data )
                  {
      
                  }
      
                  public static bool TryParse( byte[] data )
                  {
                      if ( !data.StartsWith( Prefix ) )
                          return false;
                      if ( !data.StartsWith( MessagePrefix, Prefix.Length ) )
                          return false;
      
                      return true;
                  }
      
                  public static P2PKeepAliveMessage GetMessage()
                  {
                      if ( _message == null )
                      {
                          var data = JoinBytes( Prefix, MessagePrefix );
                          _message = new P2PKeepAliveMessage( data );
                      }
      
                      return _message;
                  }
              }
      
              #endregion
          }
      }