UDP打洞实现

27
我正在尝试实现UDP打洞。我基于这篇文章WIKI页面上的理论,但是在C#编码方面遇到了一些问题。以下是我的问题:
使用在这里发布的代码后,我现在能够连接到远程机器并在同一个端口上监听传入连接(将2个UDP客户端绑定到相同的端口)。
由于某种原因,同一端口上的两个绑定会互相阻塞,从而导致无法接收任何数据。
我有一个UDP服务器来响应我的连接请求,所以如果在绑定任何其他客户端之前首先连接到它,我会得到它的回复。
如果我将另一个客户端绑定到该端口,则不会在任何客户端上接收到数据。
以下是显示我的问题的2个代码片段。第一个连接到远程服务器以在NAT设备上创建规则,然后在不同的线程上启动监听器以捕获传入的数据包。代码然后发送数据包到本地IP,以便监听器可以获取它。第二个只向本地IP发送数据包以确保它起作用。我知道这不是实际的打洞,因为我将数据包发送给自己,而根本没有离开NAT设备。在这一点上,我面临着一个问题,如果我使用在NAT设备之外的计算机进行连接,我想象这也不会有任何不同。
[编辑] 2/4/2012 我尝试使用我的网络中的另一台计算机和WireShark(数据包嗅探器)来测试监听器。我看到了来自其他计算机的传入数据包,但是它们没有被监听器UDP客户端(udpServer)或发送者UDP客户端(client)接收到。
[编辑] 2/5/2010 我现在已经添加了一个函数调用,用于在初始发送和接收数据包后关闭第一个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();
}

当涉及到IP或端口的数据包丢失时,应该如何处理? - user1914692
+1 对于有用文章的链接。我已经寻找了相当长时间的全面讨论! - David Mertens
5个回答

21

如果我理解得正确,你正在尝试在两个不同NAT下的客户端之间进行点对点通信,并使用调解服务器进行洞穿?

几年前我用c#做过完全相同的事情,虽然我还没有找到那段代码,但如果你需要,我可以给你一些提示:

首先,我不会在udpclient上使用Connect()函数,因为UDP是一种无连接协议,这个函数实际上只是隐藏了UDP套接字的功能。

你应该执行以下步骤:

  1. 在一个服务器上开放一个UDP套接字,并绑定到一个特定的端口(例如将此套接字绑定到选择的端口,例如23000)
  2. 在第一个客户端上创建一个UDP套接字,并向服务器发送一些内容,地址为23000。 不要绑定此套接字。当使用UDP发送数据包时,Windows会自动为套接字分配一个可用端口
  3. 从另一个客户端执行相同的操作
  4. 服务器现在从2个地址处接收到了2个客户端的2个数据包,并且每个数据包都带有不同的端口号。测试服务器是否可以在相同的地址和端口上回传数据包。(如果这不起作用,则说明某些地方出错了,或者您的NAT无法正常工作。如果您可以玩游戏而不用打开端口,则代表您的NAT正在工作:D)
  5. 服务器现在应该将另一个客户端的地址和端口发送给每个连接的客户端。
  6. 现在,客户端应该能够使用UDP向从服务器收到的地址发送数据包。

你应该注意,NAT使用的端口可能与客户端PC上使用的端口不同!!服务器应该将此外部端口分发给客户端。你必须使用外部地址和外部端口进行发送!

还要注意,您的NAT可能不支持此类端口转发。一些NAT将所有传入的流量都转发到指定端口的客户端上,这就是您想要的。但是,一些NAT会对传入的数据包地址进行过滤,因此可能会阻止其他客户端的数据包。然而,当使用标准个人用户路由器时,这种情况不太可能发生。


1
谢谢你的回答!我会尝试一下。我想我可能没有尝试过在两个方向上发送数据包... - brooc
3
好的,请告诉我是否成功运行了。 另外一件事,我的理解是你在将多个套接字绑定到同一个端口上?在大多数情况下,每个端口应该只使用一个套接字,但我不知道是否可以绑定多个套接字 :D - MHGameWork
“不要绑定这个套接字”的建议对我帮助很大。我的应用程序在NAT后面被绑定到了一个硬编码的端口,但似乎NAT已经为我的应用绑定了另一个随机端口,而我并不知道这一点。服务器发送的消息是发送到我的硬编码端口,而NAT却丢弃了这些数据包。 - Zhuravlev A.

5

编辑:经过更多的测试,除非启用UPnP,否则这对我根本不起作用。因此,我在这里写的许多内容可能对您有用,但许多人没有启用UPnP(因为它是安全风险),所以它对他们没有用。

以下是使用PubNub作为中继服务器的一些代码 :). 我不建议在未经测试的情况下使用此代码,因为它并不完美(我不确定它是否安全或者做事情的正确方式?我不是网络专家),但它应该给你一个想法。至少,在我的业余项目中,它已经成功地工作了一段时间。它缺少的东西是:

  • 测试客户端是否在您的LAN上。我只是发送到两个位置,这适用于您的LAN和另一个网络上的设备,但效率很低。
  • 测试客户端停止侦听时的情况,例如,他们关闭了程序。因为这是UDP,所以它是无状态的,所以我们发送消息到虚空中并不重要,但如果没有人收到它们,我们可能不应该这样做
  • 我使用Open.NAT来进行程序化的端口转发,但这可能在某些设备上不起作用。具体来说,它使用UPnP,这有点不安全,并要求手动转发UDP端口1900。一旦他们这样做了,它就支持大多数路由器,但许多人尚未这样做。

首先,您需要一种获取外部和本地IP的方法。以下是获取本地IP的代码:

// From https://dev59.com/nmw15IYBdhLWcg3wIoVM
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");
}

以下是一些通过尝试返回设计用于返回外部IP的网站来获取您的外部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 "";

}

现在我们需要找到一个开放的端口并将其转发到外部端口。如上所述,我使用了Open.NAT。首先,在查看注册的UDP端口后,您可以组合出一份您认为适合您的应用程序使用的端口列表。以下是几个示例:

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

现在是PubNub中继服务器代码(P2PPeer将在下面定义)。这里有很多内容,所以我不会详细解释,但希望代码足够清晰,能帮助您理解正在发生的事情。
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);
}

这是一个P2P节点。
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;

欢迎提出评论和问题,如果这里有不好的实践或者不起作用的地方,请随意给予反馈。在翻译过程中引入了一些错误,我会在这里逐步修复,但至少可以让你了解该怎么做。


3

您尝试过使用异步函数吗?以下是一个示例,展示了如何使其正常工作。您可能需要进行一些调整,以确保其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);
    }

我希望这能对您有所帮助。

在配置服务器时,将外部IP地址放在哪里?当连接到服务器时,将对等方的外部IP地址放在哪里? - mahmoud nezar sarhan

1

更新:

无论哪个UdpClients先绑定,都会由Windows发送传入的数据包。在您的示例中,请尝试将设置侦听线程的代码块移动到顶部。

您确定问题不仅仅是接收线程只能处理单个接收吗?请尝试使用以下内容替换接收线程。

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

我没有收到第一个数据包。线程在接收时被阻塞,将接收放入while循环中不起作用。此外,我确实看到数据包通过数据包嗅探器传递到我的PC,但没有传递到客户端。您是否有UDP打洞的工作示例? - brooc
刚刚尝试了一下,在打印行上设置了断点,但程序从未到达该处。 - brooc
理论上,创建NAT绑定所需的全部操作只是从私有套接字发送一个数据包到公共套接字。之后,NAT应该将来自公共套接字的所有数据包传递到私有套接字(至少在绑定超时之前,最小值为X分钟)。听起来这就是您正在做的,所以它应该可以正常工作。 - sipsorcery
目前我没有遇到NAT问题,因为我正在私有网络内发送和接收数据包。此外,我看到数据包到达PC,但没有被任何UDP套接字终止,而是被丢弃了。似乎由于某种原因,这两个UDP客户端互相阻塞,无法接收任何内容。 - brooc
我现在已经添加了一个函数调用,以便在最初的数据包发送和接收后关闭第一个UDP客户端,只留下第二个UDP客户端监听该端口。这很有效,我可以在该端口上从网络内部接收数据包。我现在将尝试从网络外部接收数据包。 - brooc

1

非常抱歉上传了如此庞大的代码,但我认为这很清楚地解释了事情的工作原理,并且可能非常有用。如果您在使用此代码时遇到问题,请告诉我。

注意:

  1. 这只是一个草稿
  2. (重要)您必须使用本地端点通知服务器。如果您不这样做,即使服务器在NAT之外,您也将无法在两个位于同一NAT后面的对等方之间进行通信(例如,在一台本地计算机上)
  3. 您必须关闭“puncher”客户端(至少我在关闭它之前无法接收任何数据包)。稍后,您将能够使用其他UdpClient与服务器通信
  4. 当然,这不适用于对称NAT
  5. 如果您发现此代码中的某些内容是“可怕的惯例”,请告诉我,因为我不是网络专家 :)

Server.cs

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

Client.cs

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

Demo.cs

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

Protocol.cs

我不确定这种方法有多好,也许像 protobuf 这样的工具可以做得更好。

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
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接