Sorry for uploading such a huge piece of code, but i guess this is very clearly explains how things work, and may be really useful. If you will have issues with this code, please let me know.
Note:
- this is just a draft
- (important) you MUST inform server with your local endpoint. if you will not do it you will not be able to communicate between two peers behind one NAT (for example, on one local machine) even if the server is out of NAT
- you must close "puncher" client (at least, i did not manage to receive any packets until i did it). later you will be able to communicate with server using other
UdpClient
's
- of cource it will not work with symmetric NAT
- if you find that something in this code is "terrible practice", please tell me, i'm not network expert :)
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();
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;
_localEndPoint = new IPEndPoint( IPAddress.Parse( GetLocalIp() ), localPort );
}
public void Start( )
{
_udpPuncher = new UdpClient();
_udpClient = new UdpClient();
_extraUdpClient = new UdpClient();
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)
{
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 )
{
if ( _partnerPublicUdpEndPoint == null && _partnerLocalUdpEndPoint == null )
{
_udpPuncher.Send( messageToServer.Data, messageToServer.Data.Length, _serverUdpEndPoint );
Console.WriteLine( $" >>> Sent UDP to server [ {_serverUdpEndPoint.Address} : {_serverUdpEndPoint.Port} ]" );
}
else
{
_udpClient.Send( messageToServer.Data, messageToServer.Data.Length, _serverUdpEndPoint );
Console.WriteLine( $" >>> Sent UDP to server [ {_serverUdpEndPoint.Address} : {_serverUdpEndPoint.Port} ]" );
_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} ]" );
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
{
UdpClient udpClient = _partnerPublicUdpEndPoint == null ? _udpPuncher : _udpClient;
var receivedResults = await udpClient.ReceiveAsync();
if ( !_isRunning )
{
break;
}
ProcessUdpMessage( receivedResults.Buffer, receivedResults.RemoteEndPoint );
}
catch ( SocketException ex )
{
}
catch ( Exception ex )
{
Console.WriteLine( $"Error: {ex.Message}" );
}
}
}
private static void ProcessUdpMessage( byte[] buffer, IPEndPoint remoteEndPoint )
{
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();
}
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
I'm not sure how good this approach, maybe something like protobuf can do it better
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
}
}