Direct connection support

main
Thomas Woischnig 2023-12-04 01:05:29 +01:00
parent a99b097a83
commit fc25760c2f
12 changed files with 397 additions and 40 deletions

View File

@ -4,6 +4,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
@ -18,6 +19,8 @@ namespace Lobbies
AutoResetEvent waitForExternalIp = new AutoResetEvent(false);
UdpEchoServer udpEchoServer = new UdpEchoServer();
private Dictionary<Guid, LobbyInfo> lobbyInformation = new Dictionary<Guid, LobbyInfo>();
private string? host;
private int port;
@ -75,6 +78,8 @@ namespace Lobbies
public void HostLobby(Guid gameId, string name, int gameMode, int maxPlayerCount, string? password, string? ip, int port)
{
udpEchoServer.Start(0);
byte[]? hash = null, salt = null;
if(!string.IsNullOrEmpty(password))
@ -91,8 +96,9 @@ namespace Lobbies
PlayerCount = 0,
PasswordHash = hash,
PasswordSalt = salt,
HostIp = ip,
HostPort = port
HostIps = GatherLocalIpAddresses().ToArray(),
HostPort = port,
HostTryPort = udpEchoServer.Port
};
byte[] messageData = bufferRental.Rent();
@ -142,7 +148,7 @@ namespace Lobbies
});
}
public void UpdateLobby(string name, int gameMode, int maxPlayerCount, int playerCount, string? password, string? ip, int port)
public void UpdateLobby(string name, int gameMode, int maxPlayerCount, int playerCount, string? password, int port)
{
byte[]? hash = null, salt = null;
@ -159,8 +165,9 @@ namespace Lobbies
PlayerCount = playerCount,
PasswordHash = hash,
PasswordSalt = salt,
HostIp = ip,
HostPort = port
HostIps = GatherLocalIpAddresses().ToArray(),
HostPort = port,
HostTryPort = udpEchoServer.Port
};
byte[] messageData = bufferRental.Rent();
@ -170,6 +177,8 @@ namespace Lobbies
public void CloseLobby()
{
udpEchoServer.Stop();
var lobbyDelete = new LobbyDelete()
{
@ -207,6 +216,32 @@ namespace Lobbies
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public async Task<IPAddress?> TryDirectConnection(IPAddress[] ipAddressesToTry, int tryPort)
{
return await Task.Run(() =>
{
IPAddress? ret = null;
using (var udpEchoClient = new UdpEchoServer())
{
udpEchoClient.Reached += (ep) =>
{
ret = ep.Address;
};
udpEchoClient.Start(0);
foreach (var ip in ipAddressesToTry)
{
udpEchoClient.CheckConnectionPossible(new IPEndPoint(ip, tryPort));
}
Thread.Sleep(500);
}
return ret;
});
}
public static IPAddress[] GetIPsByName(string hostName, bool ip4Wanted, bool ip6Wanted)
{
// Check if the hostname is already an IPAddress
@ -214,7 +249,7 @@ namespace Lobbies
if (IPAddress.TryParse(hostName, out outIpAddress) == true)
return new IPAddress[] { outIpAddress };
//<----------
IPAddress[] addresslist = Dns.GetHostAddresses(hostName);
if (addresslist == null || addresslist.Length == 0)
@ -361,10 +396,24 @@ namespace Lobbies
catch { }
}
public IEnumerable<IPAddress> GatherLocalIpAddresses()
{
foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces())
{
IPInterfaceProperties ipProps = netInterface.GetIPProperties();
foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses)
{
yield return addr.Address;
}
}
}
public void Dispose()
{
waitForExternalIp.Dispose();
tcpClient.Dispose();
udpEchoServer.Dispose();
}
}

View File

@ -0,0 +1,197 @@
using System.Net.Sockets;
using System.Net;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Lobbies
{
/// <summary>
/// Small udp server to receive udp packets and echo the data back to source
/// </summary>
internal class UdpEchoServer : IDisposable
{
public const int SIO_UDP_CONNRESET = -1744830452;
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
private bool running = false;
private bool isDisposed = false;
private UdpClient? serverSocketV4, serverSocketV6;
public delegate void ReachableEventArgs(IPEndPoint remoteEndpoint);
/// <summary>
/// If a valid request for a ip and port query comes in call this event with the seen remote ip and port for a lobby server client id
/// </summary>
public event ReachableEventArgs? Reached;
public int Port { get; private set; }
private bool isRunningV4 = false, isRunningV6 = false;
public void CheckConnectionPossible(IPEndPoint remoteEndpoint)
{
if (!running || serverSocketV4 == null || serverSocketV6 == null)
throw new Exception("Listener not running!");
byte[] magicRequest = new byte[] { 0 };
for (int i = 0; i < 16; i++)
if(remoteEndpoint.AddressFamily == AddressFamily.InterNetwork)
serverSocketV4.Send(magicRequest, magicRequest.Length, remoteEndpoint);
else
serverSocketV6.Send(magicRequest, magicRequest.Length, remoteEndpoint);
}
/// <summary>
/// Listen to requests and fire events
/// </summary>
/// <param name="port">The port to listen on</param>
private async void ListenV4(int port)
{
try
{
cancellationTokenSource.Token.ThrowIfCancellationRequested();
serverSocketV4 = new UdpClient(port, AddressFamily.InterNetwork);
serverSocketV4.Client.IOControl(
(IOControlCode)SIO_UDP_CONNRESET,
new byte[] { 0, 0, 0, 0 },
null
);
Port = ((IPEndPoint)serverSocketV4.Client.LocalEndPoint).Port;
byte[] magicAnswer = new byte[] { 1 };
using (cancellationTokenSource.Token.Register(() => { serverSocketV4.Close(); }))
{
isRunningV4 = true;
while (running)
{
var receiveResult = await serverSocketV4.ReceiveAsync();
if (receiveResult.Buffer.Length == 1)
{
if (receiveResult.Buffer[0] == 0)
{
for (int i = 0; i < 16; i++)
serverSocketV4.Send(magicAnswer, magicAnswer.Length, receiveResult.RemoteEndPoint);
}
if (receiveResult.Buffer[0] == 1)
{
Reached?.Invoke(receiveResult.RemoteEndPoint);
}
}
}
}
}
catch when (cancellationTokenSource.IsCancellationRequested || !running) //Cancel requested
{
}
catch
{
throw;
}
finally
{
serverSocketV4?.Dispose();
serverSocketV4 = null;
running = false;
isRunningV4 = false;
}
}
/// <summary>
/// Listen to requests and fire events
/// </summary>
/// <param name="port">The port to listen on</param>
private async void ListenV6(int port)
{
try
{
cancellationTokenSource.Token.ThrowIfCancellationRequested();
serverSocketV6 = new UdpClient(port, AddressFamily.InterNetworkV6);
serverSocketV6.Client.IOControl(
(IOControlCode)SIO_UDP_CONNRESET,
new byte[] { 0, 0, 0, 0 },
null
);
byte[] magicAnswer = new byte[] { 1 };
using (cancellationTokenSource.Token.Register(() => { serverSocketV6.Close(); }))
{
isRunningV6 = true;
while (running)
{
var receiveResult = await serverSocketV6.ReceiveAsync();
if (receiveResult.Buffer.Length == 1)
{
if (receiveResult.Buffer[0] == 0)
{
Reached?.Invoke(receiveResult.RemoteEndPoint);
}
if (receiveResult.Buffer[0] == 1)
{
for (int i = 0; i < 16; i++)
serverSocketV6.Send(magicAnswer, magicAnswer.Length, receiveResult.RemoteEndPoint);
}
}
}
}
}
catch when (cancellationTokenSource.IsCancellationRequested || !running) //Cancel requested
{
}
catch
{
throw;
}
finally
{
serverSocketV6?.Dispose();
serverSocketV6 = null;
running = false;
isRunningV6 = false;
}
}
/// <summary>
/// Start udp listener
/// </summary>
/// <param name="port">The port to listen on</param>
public void Start(int port)
{
isRunningV4 = false;
isRunningV6 = false;
running = true;
_ = Task.Run(() => ListenV4(port));
while (running && !isRunningV4)
Thread.Yield();
_ = Task.Run(() => ListenV6(Port));
while (running && (!isRunningV4 || !isRunningV6))
Thread.Yield();
}
/// <summary>
/// Stop udp listener
/// </summary>
public void Stop()
{
running = false;
cancellationTokenSource.Cancel();
if (serverSocketV4 != null)
serverSocketV4?.Close();
if (serverSocketV6 != null)
serverSocketV6?.Close();
}
public void Dispose()
{
if (!isDisposed)
{
Stop();
while (isRunningV4 || isRunningV6)
Task.Yield();
cancellationTokenSource.Dispose();
isDisposed = true;
}
}
}
}

View File

@ -3,6 +3,7 @@ using Lobbies;
using LobbyClientTest;
using LobbyServerDto;
using System.Net;
using System.Net.WebSockets;
Console.WriteLine("Starting lobby client v0.7!");
var lobbyClient = new LobbyClient();
@ -16,12 +17,11 @@ FakeGameHost fakeGameHost = new FakeGameHost();
int myPort = fakeGameHost.Server(0);
string? myExternalIp = null;
int myExternalPort = -1;
IPEndPoint? hostInfo = null;
bool running = true;
bool connected = false;
_ = Task.Run(() =>
_ = Task.Run(async () =>
{
while (running)
{
@ -78,8 +78,23 @@ _ = Task.Run(() =>
var lobbyHostInfo = lobbyEvent.EventData as LobbyHostInfo;
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Host info for lobby {lobbyHostInfo!.LobbyId} is {lobbyHostInfo.HostIp}:{lobbyHostInfo.HostPort}!");
hostInfo = new IPEndPoint(IPAddress.Parse(lobbyHostInfo.HostIp!), lobbyHostInfo.HostPort);
Console.WriteLine($"Host info for lobby {lobbyHostInfo!.LobbyId} is {(lobbyHostInfo.HostIps != null && lobbyHostInfo.HostIps.Length > 0 ? lobbyHostInfo.HostIps[0].ToString() : "")}:{lobbyHostInfo.HostPort}!");
//Try direct connection
if (lobbyHostInfo.HostIps != null && lobbyHostInfo.HostIps.Length > 0)
{
Console.WriteLine($"Trying direct connection to {string.Join<IPAddress>(",", lobbyHostInfo.HostIps)} on port {lobbyHostInfo.HostTryPort}!");
var reachableIp = await lobbyClient.TryDirectConnection(lobbyHostInfo.HostIps, lobbyHostInfo.HostTryPort);
if(reachableIp != null)
{
Console.WriteLine($"Direct connection to {reachableIp.ToString()} possible, using direct connection!");
Console.WriteLine($"Connecting game client!");
fakeGameHost.Send(new IPEndPoint(reachableIp, lobbyHostInfo.HostPort), "Hello from Game Client!");
Console.Write(">");
continue;
}
}
Console.WriteLine($"Requesting nat punch to me!");
lobbyClient.RequestLobbyNatPunch(lobbyHostInfo.LobbyId, null, (remoteEndpoint, messageBuffer, messageLength) => {
fakeGameHost.Send(remoteEndpoint, messageBuffer, messageLength);
@ -111,7 +126,7 @@ _ = Task.Run(() =>
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Nat punch requested to {lobbyRequestNatPunch!.ClientIp}:{lobbyRequestNatPunch.ClientPort}!");
Task.Run(() =>
_ = Task.Run(() =>
{
lobbyClient.QueryExternalIpAndPort((remoteEndpoint, messageData, messageLength) => {
fakeGameHost.Send(remoteEndpoint, messageData, messageLength);
@ -140,7 +155,6 @@ _ = Task.Run(() =>
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Nat punch request done!");
Console.WriteLine($"Connecting game client!");
fakeGameHost.Send(new IPEndPoint(IPAddress.Parse(lobbyNatPunchDone!.ExternalIp!), lobbyNatPunchDone.ExternalPort), "Hello from Game Client!");
Console.Write(">");
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
@ -24,7 +25,8 @@ namespace LobbyServer
public int MaxPlayerCount { get; set; }
public byte[]? PasswordHash { get; set; }
public byte[]? PasswordSalt { get; set; }
public required string HostIp { get; set; }
public required IPAddress[] HostIps { get; set; }
public int HostPort { get; set; }
public int HostTryPort { get; set; }
}
}

View File

@ -1,7 +1,7 @@
using LobbyServer;
using LobbyServerDto;
using System.Collections.Concurrent;
using System.Net;
using var closing = new AutoResetEvent(false);
using var tcpServer = new TcpServer();
@ -42,6 +42,11 @@ tcpServer.DataReceived += (clientId, dataLength, data) =>
if (!GameGuids.ValidGuids.Contains(lobbyCreate.GameId))
throw new Exception("Invalid game guid!");
List<IPAddress> hostIpAddresses = new List<IPAddress>();
hostIpAddresses.Add(IPAddress.Parse(tcpServer.GetClientIp(clientId)!));
if(lobbyCreate.HostIps != null)
hostIpAddresses.AddRange(lobbyCreate.HostIps);
var lobby = new Lobby
{
Name = lobbyCreate.Name,
@ -52,8 +57,9 @@ tcpServer.DataReceived += (clientId, dataLength, data) =>
PasswordHash = lobbyCreate.PasswordHash,
PasswordSalt = lobbyCreate.PasswordSalt,
HostClientId = clientId,
HostIp = lobbyCreate.HostIp == null ? tcpServer.GetClientIp(clientId)! : lobbyCreate.HostIp,
HostIps = hostIpAddresses.ToArray(),
HostPort = lobbyCreate.HostPort,
HostTryPort = lobbyCreate.HostTryPort
};
if(lobbiesByClientId.TryGetValue(clientId, out var existingLobby))
@ -113,10 +119,17 @@ tcpServer.DataReceived += (clientId, dataLength, data) =>
existingLobby.PasswordHash = lobbyUpdate.PasswordHash;
existingLobby.PasswordSalt = lobbyUpdate.PasswordSalt;
if (lobbyUpdate.HostIp != null)
existingLobby.HostIp = lobbyUpdate.HostIp;
List<IPAddress> hostIpAddresses = new List<IPAddress>();
hostIpAddresses.Add(IPAddress.Parse(tcpServer.GetClientIp(clientId)!));
if (lobbyUpdate.HostIps != null)
hostIpAddresses.AddRange(lobbyUpdate.HostIps);
if (!Enumerable.SequenceEqual(existingLobby.HostIps, hostIpAddresses))
existingLobby.HostIps = hostIpAddresses.ToArray();
existingLobby.HostPort = lobbyUpdate.HostPort;
existingLobby.HostTryPort = lobbyUpdate.HostTryPort;
_ = Task.Run(() => SendLobbyUpdate(Lobby.LobbyUpdateType.Update, existingLobby));
}
}
@ -221,7 +234,7 @@ tcpServer.DataReceived += (clientId, dataLength, data) =>
{
var messageData = bufferRental.Rent();
var lobbyHostInfo = new LobbyHostInfo() { LobbyId = lobby.Id, HostIp = lobby.HostIp, HostPort = lobby.HostPort };
var lobbyHostInfo = new LobbyHostInfo() { LobbyId = lobby.Id, HostIps = lobby.HostIps, HostPort = lobby.HostPort, HostTryPort = lobby.HostTryPort };
var messageDataLength = lobbyHostInfo.Serialize(messageData);
_ = Task.Run(async () =>
{
@ -426,7 +439,7 @@ Console.CancelKeyPress += (sender, args) =>
args.Cancel = true;
};
Console.WriteLine($"{DateTime.Now}: Application started v0.7");
Console.WriteLine($"{DateTime.Now}: Application started v0.8");
udpServer.Start(8088);
tcpServer.Start(8088);

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Net;
namespace LobbyServerDto
{
@ -41,13 +42,17 @@ namespace LobbyServerDto
[MaxLength(16)]
public byte[]? PasswordSalt { get; set; }
/// <summary>
/// The hosts ip. Used the the host information send to clients on their request.
/// The hosts ip addresses locally detected. Used the the host information send to clients on their request.
/// </summary>
[MaxLength(32)]
public string? HostIp { get; set; }
public IPAddress[]? HostIps { get; set; }
/// <summary>
/// The hosts port. Used the the host information send to clients on their request.
/// </summary>
public int HostPort { get; set; }
/// <summary>
/// The hosts echo port to try if a connection is possible.
/// </summary>
public int HostTryPort { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Net;
namespace LobbyServerDto
{
@ -13,13 +14,17 @@ namespace LobbyServerDto
/// </summary>
public Guid LobbyId { get; set; }
/// <summary>
/// The hosts ip, this could be an internal address
/// The hosts ip addresses locally detected. Used the the host information send to clients on their request.
/// </summary>
[MaxLength(32)]
public string? HostIp { get; set; }
public IPAddress[]? HostIps { get; set; }
/// <summary>
/// The hosts port
/// </summary>
public int HostPort { get; set; }
/// <summary>
/// The hosts echo port to try if a connection is possible.
/// </summary>
public int HostTryPort { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Net;
namespace LobbyServerDto
{
@ -7,7 +8,7 @@ namespace LobbyServerDto
/// </summary>
[LobbyMessage]
public partial class LobbyUpdate
{
{
/// <summary>
/// The displayname of the lobby
/// </summary>
@ -34,13 +35,17 @@ namespace LobbyServerDto
[MaxLength(16)]
public byte[]? PasswordSalt { get; set; }
/// <summary>
/// The hosts ip
/// The hosts ip addresses locally detected. Used the the host information send to clients on their request.
/// </summary>
[MaxLength(32)]
public string? HostIp { get; set; }
public IPAddress[]? HostIps { get; set; }
/// <summary>
/// The hosts port
/// The hosts port. Used the the host information send to clients on their request.
/// </summary>
public int HostPort { get; set; }
/// <summary>
/// The hosts echo port to try if a connection is possible.
/// </summary>
public int HostTryPort { get; set; }
}
}

View File

@ -74,6 +74,7 @@ namespace LobbyServerDto
s.Append(@$"// <auto-generated />
using System.Collections.Generic;
using System.Text;
using System.Net;
{(foundClass.Value.nameSpace is null ? null : $@"namespace {foundClass.Value.nameSpace}
{{")}
@ -113,6 +114,34 @@ using System.Text;
var name = p.Identifier.ToString();
switch(p.Type.ToString())
{
case "IPAddress[]":
case "IPAddress[]?":
s.Append($@"
if ({name} != null)
{{
int maxLength = Math.Min({name}.Length, {maxLength});
uint v = (uint)maxLength;
while (v >= 0x80)
{{
buffer[offset++] = (byte)(v | 0x80);
v >>= 7;
}}
buffer[offset++] = (byte)v;
for(int i = 0; i < maxLength; i++)
{{
var ipBuffer = {name}[i].GetAddressBytes();
buffer[offset++] = (byte)ipBuffer.Length;
Buffer.BlockCopy(ipBuffer, 0, buffer, offset, ipBuffer.Length);
offset += ipBuffer.Length;
}}
}}
else
{{
buffer[offset++] = 0;
}}
");
break;
case "bool":
s.Append($@"
buffer[offset++] = (byte)({name} == true ? 1 : 0);");
@ -226,6 +255,42 @@ using System.Text;
var name = p.Identifier.ToString();
switch (p.Type.ToString())
{
case "IPAddress[]":
case "IPAddress[]?":
s.Append($@"
{{
int arrayLen = 0;
int shift = 0;
byte b;
do {{
// Check for a corrupted stream. Read a max of 5 bytes.
// In a future version, add a DataFormatException.
if (shift == 5 * 7) // 5 bytes max per Int32, shift += 7
throw new FormatException(""Format_Bad7BitInt32"");
// ReadByte handles end of stream cases for us.
b = buffer[offset++];
arrayLen |= (b & 0x7F) << shift;
shift += 7;
}} while ((b & 0x80) != 0);
if(arrayLen > {maxLength})
throw new FormatException(""Format_IPAddressArrayToLong"");
if(arrayLen > 0)
{{
ret.{name} = new IPAddress[arrayLen];
for(int i=0; i < arrayLen; i++)
{{
var itemLen = buffer[offset++];
if(itemLen > 16)
throw new FormatException(""Format_IPAddressBytesArrayToLong"");
ret.{name}[i] = new IPAddress(buffer.Slice(offset, itemLen).ToArray());
offset += itemLen;
}}
}}
}}");
break;
case "bool":
s.Append($@"
ret.{name} = buffer[offset++] == 0 ? false : true;");
@ -236,10 +301,10 @@ using System.Text;
break;
case "Guid":
s.Append($@"
{{
ret.{name} = new Guid(buffer.Slice(offset, 16).ToArray());
offset+=16;
}}");
{{
ret.{name} = new Guid(buffer.Slice(offset, 16).ToArray());
offset+=16;
}}");
break;
case "string":
case "string?":
@ -260,6 +325,9 @@ using System.Text;
shift += 7;
}} while ((b & 0x80) != 0);
if(strLen > {maxLength})
throw new FormatException(""Format_StringToLong"");
if(strLen > 0)
{{
ret.{name} = Encoding.UTF8.GetString(buffer.Slice(offset, strLen).ToArray());
@ -271,7 +339,7 @@ using System.Text;
case "byte[]?":
s.Append($@"
{{
int strLen = 0;
int byteLen = 0;
int shift = 0;
byte b;
do {{
@ -282,14 +350,17 @@ using System.Text;
// ReadByte handles end of stream cases for us.
b = buffer[offset++];
strLen |= (b & 0x7F) << shift;
byteLen |= (b & 0x7F) << shift;
shift += 7;
}} while ((b & 0x80) != 0);
if(strLen > 0)
if(byteLen > {maxLength})
throw new FormatException(""Format_ByteArrayToLong"");
if(byteLen > 0)
{{
ret.{name} = buffer.Slice(offset, strLen).ToArray();
offset += strLen;
ret.{name} = buffer.Slice(offset, byteLen).ToArray();
offset += byteLen;
}}
}}");
break;

View File

@ -15,10 +15,6 @@
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
</ItemGroup>
<ItemGroup>
<None Remove="bin\Release\netstandard2.0\\LobbyServerSourceGenerator.dll" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />