c# udp traversal nat hole-punching

c# - Implementación de perforación UDP



traversal nat (5)

Estoy tratando de lograr la perforación de agujeros UDP. Estoy basando mi teoría en este artículo y en esta página de WIKI , pero estoy enfrentando algunos problemas con la codificación C # de la misma. Aquí está mi problema:

Utilizando el código publicado here , ahora puedo conectarme a una máquina remota y escuchar en el mismo puerto las conexiones entrantes (vincular 2 clientes UDP al mismo puerto).

Por alguna razón, los dos enlaces al mismo puerto bloquean entre sí la recepción de cualquier dato. Tengo un servidor UDP que responde a mi conexión, así que si me conecto primero antes de vincular a cualquier otro cliente al puerto, recibo sus respuestas.

Si vinculo a otro cliente al puerto, no se recibirán datos en ninguno de los clientes.

Los siguientes son 2 piezas de código que muestran mi problema. El primero se conecta a un servidor remoto para crear la regla en el dispositivo NAT y luego se inicia una escucha en un hilo diferente para capturar los paquetes entrantes. El código luego envía los paquetes a la IP local para que el oyente los reciba. El segundo solo envía paquetes a la IP local para asegurarse de que esto funcione. Sé que este no es el verdadero agujero de perforación, ya que me envío los paquetes sin tener que vivir el dispositivo NAT. Me estoy enfrentando a un problema en este punto, y no creo que esto sea diferente si utilizo una computadora fuera del dispositivo NAT para conectarme.

[EDITAR] 2/4/2012 Intenté usar otra computadora en mi red y WireShark (detector de paquetes) para probar al oyente. Veo los paquetes entrantes desde la otra computadora pero no son recibidos por el cliente UDP del oyente (udpServer) o el cliente UDP del remitente (cliente).

[EDITAR] 5/2/2010 Ahora he agregado una llamada a la función para cerrar el primer cliente UDP después del envío y la recepción iniciales de paquetes que solo viven en el segundo cliente UDP para escuchar en el puerto. Esto funciona y puedo recibir paquetes desde dentro de la red en ese puerto. Ahora intentaré enviar y recibir paquetes fuera de la red. Publicaré mis conclusiones tan pronto como encuentre algo.

Usando este código obtengo datos sobre el cliente que escucha:

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

Si uso el siguiente código, después de la conexión y la transferencia de datos entre mi cliente y el servidor, el cliente UDP que escucha no recibirá nada:

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


Edición: después de muchas más pruebas, esto no parece funcionar en absoluto para mí a menos que habilite UPnP. Por lo tanto, muchas de las cosas que escribí aquí pueden ser útiles, pero muchas personas no tienen habilitado el UPnP (porque es un riesgo de seguridad), por lo que no funcionará para ellos.

Aquí hay un código que usa PubNub como servidor de retransmisión :). No recomiendo usar este código sin probar porque no es perfecto (no estoy seguro de si es seguro o es la forma correcta de hacer las cosas. No soy un experto en redes), pero debería darle una idea. de que hacer Al menos me ha funcionado hasta ahora en un proyecto de hobby. Las cosas que falta son:

  • Comprobando si el cliente está en su LAN. Acabo de enviar a ambos, lo que funciona para su LAN y un dispositivo en otra red, pero eso es muy ineficiente.
  • Probar cuando el cliente deja de escuchar si, por ejemplo, cerraron el programa. Debido a que esto es UDP, es sin estado, así que no importa si estamos enviando mensajes al vacío, pero probablemente no deberíamos hacerlo si nadie los recibe.
  • Utilizo Open.NAT para realizar el reenvío de puertos mediante programación, pero esto podría no funcionar en algunos dispositivos. Específicamente, utiliza UPnP, que es un poco inseguro y requiere que el puerto UDP 1900 se reenvíe manualmente. Una vez que lo hacen, es compatible con la mayoría de los enrutadores, pero muchos aún no lo han hecho.

Entonces, en primer lugar, necesita una forma de obtener sus IP locales y externas. Aquí está el código para obtener su IP local:

// From http://.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"); }

Y aquí hay un código para obtener su IP externa a través de probar algunos sitios web que están diseñados para devolver su IP externa

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

Ahora necesitamos encontrar un puerto abierto y reenviarlo a un puerto externo. Como se mencionó anteriormente utilicé Open.NAT . Primero, reúne una lista de puertos que crees que serían razonables para que tu aplicación use después de ver los puertos UDP registrados . Aquí hay algunos por ejemplo:

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

Ahora podemos recorrerlos y, con suerte, encontrar uno que no esté en uso para usar el reenvío de puertos en:

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

Ahora, para el código del servidor de relé PubNub (P2PPeer se definirá más adelante). Hay mucho aquí, así que realmente no lo voy a explicar, pero espero que el código sea lo suficientemente claro para ayudarte a entender lo que está pasando

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

Y aquí hay un 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); } } }

Finalmente, aquí están todos mis usos:

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;

Estoy abierto a comentarios y preguntas, siéntase libre de dar comentarios si algo aquí es una mala práctica o no funciona. Se introdujeron algunos errores en la traducción de mi código que finalmente solucionaré aquí, pero al menos debería darte una idea de qué hacer.


Lo siento por haber subido un código tan enorme, pero supongo que esto explica muy claramente cómo funcionan las cosas, y puede ser realmente útil. Si tiene problemas con este código, hágamelo saber.

Nota:

  1. esto es solo un borrador
  2. (importante) DEBE informar al servidor con su punto final local. Si no lo hace, no podrá comunicarse entre dos pares detrás de un NAT (por ejemplo, en una máquina local) incluso si el servidor no tiene NAT.
  3. debe cerrar el cliente "puncher" (al menos, no pude recibir ningún paquete hasta que lo hice). Más tarde, podrá comunicarse con el servidor utilizando otros UdpClient ''s.
  4. Por supuesto, no funcionará con NAT simétrica.
  5. Si encuentra que algo en este código es una "práctica terrible", dígame, no soy un experto en redes :)

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

Protocolo.cs

No estoy seguro de lo bueno que es este enfoque, tal vez algo como protobuf puede hacerlo mejor

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


Si entiendo correctamente, ¿estás tratando de comunicarte de igual a igual entre 2 clientes, cada uno detrás de un NAT diferente, utilizando un servidor de mediación para perforar agujeros?

Hace algunos años hice exactamente lo mismo en c #, todavía no he encontrado el código, pero le daré algunos consejos si lo desea:

Primero, no usaría la función Conectar () en el udpclient, ya que UDP es un protocolo sin conexión, toda esta función realmente es ocultar la funcionalidad de un socket UDP.

Debes realizar los siguientes pasos:

  1. Abra un socket UDP en un servidor con sus puertos no bloqueados por un firewall, en un puerto específico (por ejemplo, enlace este socket a un puerto elegido, por ejemplo, 23000)
  2. Cree un socket UDP en el primer cliente y envíe algo al servidor en 23000. No vincule este socket . Cuando se usa un udp para enviar un paquete, Windows asignará automáticamente un puerto libre al socket
  3. Haz lo mismo desde el otro cliente.
  4. El servidor ahora ha recibido 2 paquetes de 2 clientes en 2 direcciones diferentes con 2 puertos diferentes. Pruebe si el servidor puede enviar paquetes de nuevo en la misma dirección y puerto. (Si esto no funciona, hiciste algo mal o tu NAT no funciona. Sabes que funciona si puedes jugar sin abrir puertos: D)
  5. El servidor ahora debe enviar la dirección y el puerto de los otros clientes a cada cliente conectado.
  6. Un cliente ahora debería poder enviar paquetes usando UDP a las direcciones recibidas del servidor.

¡Debe tener en cuenta que el puerto utilizado en el nat probablemente no sea el mismo puerto que en su PC cliente! El servidor debe distribuir este puerto externo a los clientes. Debe utilizar las direcciones externas y los puertos externos para enviar a!

También tenga en cuenta que su NAT podría no admitir este tipo de reenvío de puertos. Algunos NAT reenvían todo el tráfico entrante en un puerto asignado a su cliente, que es lo que desea. Pero algunos nats filtran las direcciones de los paquetes entrantes, por lo que podría bloquear los paquetes de otros clientes. Esto es poco probable aunque al usar un enrutador de usuario personal estándar.


Si ha intentado usar las funciones asíncronas, aquí tiene un ejemplo de cómo podría hacer que funcione. Es posible que necesite un poco de trabajo para que sea 100% funcional:

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

Espero que esto ayude.


Actualizar:

Cualquiera de los enlaces de UdpClients primero es el que Windows enviará los paquetes entrantes. En su ejemplo, intente mover el bloque de código que configura el hilo de escucha a la parte superior.

¿Está seguro de que el problema no es solo que el subproceso de recepción solo se escribe para manejar una única recepción? Intente reemplazar el hilo de recepción con lo siguiente.

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