LRM Server-Side NAT Punchtrough code

This commit is contained in:
Derek S 2021-04-03 19:50:53 -05:00
parent cd309b2a93
commit 45f82b812d
6 changed files with 264 additions and 82 deletions

View file

@ -0,0 +1,51 @@
//
// Source: https://stackoverflow.com/questions/255341/getting-multiple-keys-of-specified-value-of-a-generic-dictionary#255630
//
using System;
using System.Collections.Generic;
namespace LightReflectiveMirror
{
class BiDictionary<TFirst, TSecond>
{
IDictionary<TFirst, TSecond> firstToSecond = new Dictionary<TFirst, TSecond>();
IDictionary<TSecond, TFirst> secondToFirst = new Dictionary<TSecond, TFirst>();
public void Add(TFirst first, TSecond second)
{
if (firstToSecond.ContainsKey(first) ||
secondToFirst.ContainsKey(second))
{
throw new ArgumentException("Duplicate first or second");
}
firstToSecond.Add(first, second);
secondToFirst.Add(second, first);
}
public bool TryGetByFirst(TFirst first, out TSecond second)
{
return firstToSecond.TryGetValue(first, out second);
}
public void Remove(TFirst first)
{
secondToFirst.Remove(firstToSecond[first]);
firstToSecond.Remove(first);
}
public bool TryGetBySecond(TSecond second, out TFirst first)
{
return secondToFirst.TryGetValue(second, out first);
}
public TSecond GetByFirst(TFirst first)
{
return firstToSecond[first];
}
public TFirst GetBySecond(TSecond second)
{
return secondToFirst[second];
}
}
}

View file

@ -11,8 +11,10 @@ namespace LightReflectiveMirror
public string AuthenticationKey = "Secret Auth Key";
public int UpdateLoopTime = 10;
public int UpdateHeartbeatInterval = 100;
public bool UseEndpoint = false;
public ushort EndpointPort = 6969;
public bool EndpointServerList = false;
public bool UseEndpoint = true;
public ushort EndpointPort = 8080;
public bool EndpointServerList = true;
public bool EnableNATPunchtroughServer = true;
public ushort NATPunchtroughPort = 7776;
}
}

View file

@ -1,4 +1,7 @@
using Grapevine;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;
@ -49,26 +52,29 @@ namespace LightReflectiveMirror.Endpoints
public class EndpointServer
{
public bool Start(ushort port = 6969)
public bool Start(ushort port = 8080)
{
try
{
var server = RestServerBuilder.UseDefaults().Build();
server.Prefixes.Remove($"http://localhost:{1234}/");
server.Prefixes.Add($"http://*:{port}/");
server.Router.Options.SendExceptionMessages = false;
var config = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
server.AfterStarting += (s) =>
Action<IServiceCollection> configServices = (services) =>
{
string startup = @"
********************************************************************************
* Endpoint Server listening on "+$"{string.Join(", ", server.Prefixes)}" + @"
* Be sure to Port Forward! :^)
********************************************************************************
";
Console.WriteLine(startup);
services.AddLogging(configure => configure.AddConsole());
services.Configure<LoggerFilterOptions>(options => options.MinLevel = LogLevel.None);
};
Action<IRestServer> configServer = (server) =>
{
server.Prefixes.Add($"http://*:{port}/");
};
var server = new RestServerBuilder(new ServiceCollection(), config, configServices, configServer).Build();
server.Router.Options.SendExceptionMessages = false;
server.Start();
return true;

View file

@ -2,7 +2,10 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using LightReflectiveMirror.Endpoints;
using Mirror;
@ -25,8 +28,15 @@ namespace LightReflectiveMirror
private DateTime _startupTime;
private List<int> _currentConnections = new List<int>();
public Dictionary<int, IPEndPoint> NATConnections = new Dictionary<int, IPEndPoint>();
private BiDictionary<int, string> _pendingNATPunches = new BiDictionary<int, string>();
private int _currentHeartbeatTimer = 0;
private byte[] _NATRequest = new byte[500];
private int _NATRequestPosition = 0;
private UdpClient _punchServer;
private const string CONFIG_PATH = "config.json";
public static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
@ -52,33 +62,34 @@ namespace LightReflectiveMirror
else
{
conf = JsonConvert.DeserializeObject<Config>(File.ReadAllText(CONFIG_PATH));
WriteLogMessage("Loading Assembly... ", ConsoleColor.White, true);
try
{
var asm = Assembly.LoadFile(Directory.GetCurrentDirectory() + @"\" + conf.TransportDLL);
WriteLogMessage($"Loaded Assembly: {asm.FullName}", ConsoleColor.Green);
WriteLogMessage($"OK", ConsoleColor.Green);
WriteLogMessage("\nLoading Transport Class... ", ConsoleColor.White, true);
transport = asm.CreateInstance(conf.TransportClass) as Transport;
if (transport != null)
{
var transportClass = asm.GetType(conf.TransportClass);
WriteLogMessage("OK", ConsoleColor.Green);
WriteLogMessage($"Loaded Transport: {transportClass.Name}! Loading Methods...", ConsoleColor.Green);
WriteLogMessage("\nLoading Transport Methods... ", ConsoleColor.White, true);
CheckMethods(transportClass);
WriteLogMessage("OK", ConsoleColor.Green);
WriteLogMessage("\nInvoking Transport Methods...");
if (_awakeMethod != null)
{
_awakeMethod.Invoke(transport, null);
WriteLogMessage("Called Awake on transport.", ConsoleColor.Yellow);
}
if (_startMethod != null)
{
_awakeMethod.Invoke(transport, null);
WriteLogMessage("Called Start on transport.", ConsoleColor.Yellow);
}
_startMethod.Invoke(transport, null);
WriteLogMessage("Starting Transport...", ConsoleColor.Green);
WriteLogMessage("\nStarting Transport... ", ConsoleColor.White, true);
transport.OnServerError = (clientID, error) =>
{
@ -90,6 +101,16 @@ namespace LightReflectiveMirror
WriteLogMessage($"Transport Connected, Client: {clientID}", ConsoleColor.Cyan);
_currentConnections.Add(clientID);
_relay.ClientConnected(clientID);
if (conf.EnableNATPunchtroughServer)
{
string natID = Guid.NewGuid().ToString();
_pendingNATPunches.Add(clientID, natID);
_NATRequestPosition = 0;
_NATRequest.WriteByte(ref _NATRequestPosition, (byte)OpCodes.RequestNATConnection);
_NATRequest.WriteString(ref _NATRequestPosition, natID);
transport.ServerSend(clientID, 0, new ArraySegment<byte>(_NATRequest, 0, _NATRequestPosition));
}
};
_relay = new RelayHandler(transport.GetMaxPacketSize(0));
@ -99,32 +120,71 @@ namespace LightReflectiveMirror
{
_currentConnections.Remove(clientID);
_relay.HandleDisconnect(clientID);
if(NATConnections.ContainsKey(clientID))
NATConnections.Remove(clientID);
if(_pendingNATPunches.TryGetByFirst(clientID, out _))
_pendingNATPunches.Remove(clientID);
};
transport.ServerStart();
WriteLogMessage("Transport Started!", ConsoleColor.Green);
WriteLogMessage("OK", ConsoleColor.Green);
if (conf.UseEndpoint)
{
WriteLogMessage("\nStarting Endpoint Service... ", ConsoleColor.White, true);
var endpoint = new EndpointServer();
if (endpoint.Start(conf.EndpointPort))
WriteLogMessage("Endpoint Service Started!", ConsoleColor.Green);
{
WriteLogMessage("OK", ConsoleColor.Green);
}
else
WriteLogMessage("Endpoint failure, please run as administrator.", ConsoleColor.Red);
{
WriteLogMessage("FAILED\nPlease run as administrator or check if port is in use.", ConsoleColor.DarkRed);
}
}
if (conf.EnableNATPunchtroughServer)
{
WriteLogMessage("\nStarting NatPunchthrough Socket... ", ConsoleColor.White, true);
try
{
_punchServer = new UdpClient(conf.NATPunchtroughPort);
WriteLogMessage("OK\n", ConsoleColor.Green, true);
WriteLogMessage("\nStarting NatPunchthrough Thread... ", ConsoleColor.White, true);
var natThread = new Thread(new ThreadStart(RunNATPunchLoop));
try
{
natThread.Start();
}catch(Exception err)
{
WriteLogMessage("FAILED\n" + err, ConsoleColor.DarkRed);
}
}
catch(Exception error)
{
WriteLogMessage("FAILED\nCheck if port is in use.", ConsoleColor.DarkRed, true);
Console.WriteLine(error);
}
}
}
else
{
WriteLogMessage("Transport Class not found! Please make sure to include namespaces.", ConsoleColor.Red);
WriteLogMessage("FAILED\nClass not found, make sure to included namespaces!", ConsoleColor.DarkRed);
Console.ReadKey();
Environment.Exit(0);
}
}
catch(Exception e)
{
WriteLogMessage("Exception: " + e, ConsoleColor.Red);
WriteLogMessage("FAILED\nException: " + e, ConsoleColor.DarkRed);
Console.ReadKey();
Environment.Exit(0);
}
@ -151,10 +211,55 @@ namespace LightReflectiveMirror
}
}
static void WriteLogMessage(string message, ConsoleColor color = ConsoleColor.White)
void RunNATPunchLoop()
{
WriteLogMessage("OK\n", ConsoleColor.Green);
IPEndPoint remoteEndpoint = new IPEndPoint(IPAddress.Any, conf.NATPunchtroughPort);
// Stock Data server sends to everyone:
var serverResponse = new byte[1] { 1 };
byte[] readData;
bool isConnectionEstablishment;
int pos;
string connectionID;
while (true)
{
readData = _punchServer.Receive(ref remoteEndpoint);
pos = 0;
try
{
isConnectionEstablishment = readData.ReadBool(ref pos);
if (isConnectionEstablishment)
{
connectionID = readData.ReadString(ref pos);
if (_pendingNATPunches.TryGetBySecond(connectionID, out pos))
{
NATConnections.Add(pos, new IPEndPoint(remoteEndpoint.Address, remoteEndpoint.Port));
_pendingNATPunches.Remove(pos);
Console.WriteLine("Client Successfully Established Puncher Connection. " + remoteEndpoint.ToString());
}
}
_punchServer.Send(serverResponse, 1, remoteEndpoint);
}
catch
{
// ignore, packet got fucked up or something.
}
}
}
static void WriteLogMessage(string message, ConsoleColor color = ConsoleColor.White, bool oneLine = false)
{
Console.ForegroundColor = color;
Console.WriteLine(message);
if (oneLine)
Console.Write(message);
else
Console.WriteLine(message);
}
void CheckMethods(Type type)
@ -163,11 +268,6 @@ namespace LightReflectiveMirror
_startMethod = type.GetMethod("Start", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
_updateMethod = type.GetMethod("Update", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
_lateUpdateMethod = type.GetMethod("LateUpdate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (_awakeMethod != null) WriteLogMessage("'Awake' Loaded!", ConsoleColor.Yellow);
if (_startMethod != null) WriteLogMessage("'Start' Loaded!", ConsoleColor.Yellow);
if (_updateMethod != null) WriteLogMessage("'Update' Loaded!", ConsoleColor.Yellow);
if (_lateUpdateMethod != null) WriteLogMessage("'LateUpdate' Loaded!", ConsoleColor.Yellow);
}
void WriteTitle()
@ -184,8 +284,8 @@ namespace LightReflectiveMirror
";
string load = $"Chimp Event Listener Initializing... OK" +
"\nHarambe Memorial Initializing... OK" +
"\nBananas initializing... OK\n";
"\nHarambe Memorial Initializing... OK" +
"\nBananas initializing... OK\n";
WriteLogMessage(t, ConsoleColor.Green);
WriteLogMessage(load, ConsoleColor.Cyan);

View file

@ -1,6 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Net;
using System.Text;
namespace LightReflectiveMirror
@ -57,7 +58,7 @@ namespace LightReflectiveMirror
switch (opcode)
{
case OpCodes.CreateRoom:
CreateRoom(clientId, data.ReadInt(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos));
CreateRoom(clientId, data.ReadInt(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadInt(ref pos));
break;
case OpCodes.RequestID:
SendClientID(clientId);
@ -66,7 +67,7 @@ namespace LightReflectiveMirror
LeaveRoom(clientId);
break;
case OpCodes.JoinServer:
JoinRoom(clientId, data.ReadInt(ref pos));
JoinRoom(clientId, data.ReadInt(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos));
break;
case OpCodes.KickPlayer:
LeaveRoom(data.ReadInt(ref pos), clientId);
@ -74,9 +75,6 @@ namespace LightReflectiveMirror
case OpCodes.SendData:
ProcessData(clientId, data.ReadBytes(ref pos), channel, data.ReadInt(ref pos));
break;
case OpCodes.RequestServers:
SendServerList(clientId);
break;
case OpCodes.UpdateRoomData:
var plyRoom = GetRoomForPlayer(clientId);
@ -110,28 +108,6 @@ namespace LightReflectiveMirror
public void HandleDisconnect(int clientId) => LeaveRoom(clientId);
void SendServerList(int clientId)
{
int pos = 0;
var buffer = _sendBuffers.Rent(500);
buffer.WriteByte(ref pos, (byte)OpCodes.ServerListReponse);
for(int i = 0; i < rooms.Count; i++)
{
if (rooms[i].isPublic)
{
buffer.WriteBool(ref pos, true);
buffer.WriteString(ref pos, rooms[i].serverName);
buffer.WriteString(ref pos, rooms[i].serverData);
buffer.WriteInt(ref pos, rooms[i].serverId);
buffer.WriteInt(ref pos, rooms[i].maxPlayers);
buffer.WriteInt(ref pos, rooms[i].clients.Count + 1);
}
}
buffer.WriteBool(ref pos, false);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(buffer, 0, pos));
_sendBuffers.Return(buffer);
}
void ProcessData(int clientId, byte[] clientData, int channel, int sendTo = -1)
{
Room playersRoom = GetRoomForPlayer(clientId);
@ -184,7 +160,7 @@ namespace LightReflectiveMirror
return null;
}
void JoinRoom(int clientId, int serverId)
void JoinRoom(int clientId, int serverId, bool canDirectConnect, string localIP)
{
LeaveRoom(clientId);
@ -197,15 +173,44 @@ namespace LightReflectiveMirror
rooms[i].clients.Add(clientId);
int sendJoinPos = 0;
byte[] sendJoinBuffer = _sendBuffers.Rent(5);
byte[] sendJoinBuffer = _sendBuffers.Rent(500);
sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.ServerJoined);
sendJoinBuffer.WriteInt(ref sendJoinPos, clientId);
if (canDirectConnect && Program.instance.NATConnections.ContainsKey(clientId))
{
sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.DirectConnectIP);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
_sendBuffers.Return(sendJoinBuffer);
return;
if (Program.instance.NATConnections[clientId].Address.Equals(rooms[i].hostIP.Address))
sendJoinBuffer.WriteString(ref sendJoinPos, rooms[i].hostLocalIP == localIP ? "127.0.0.1" : rooms[i].hostLocalIP);
else
sendJoinBuffer.WriteString(ref sendJoinPos, rooms[i].hostIP.Address.ToString());
sendJoinBuffer.WriteInt(ref sendJoinPos, rooms[i].useNATPunch ? rooms[i].hostIP.Port : rooms[i].port);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
sendJoinPos = 0;
sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.DirectConnectIP);
Console.WriteLine(Program.instance.NATConnections[clientId].Address.ToString());
sendJoinBuffer.WriteString(ref sendJoinPos, Program.instance.NATConnections[clientId].Address.ToString());
sendJoinBuffer.WriteInt(ref sendJoinPos, Program.instance.NATConnections[clientId].Port);
Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
_sendBuffers.Return(sendJoinBuffer);
return;
}
else
{
sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.ServerJoined);
sendJoinBuffer.WriteInt(ref sendJoinPos, clientId);
Program.transport.ServerSend(clientId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment<byte>(sendJoinBuffer, 0, sendJoinPos));
_sendBuffers.Return(sendJoinBuffer);
return;
}
}
}
}
@ -220,10 +225,13 @@ namespace LightReflectiveMirror
_sendBuffers.Return(sendBuffer);
}
void CreateRoom(int clientId, int maxPlayers, string serverName, bool isPublic, string serverData)
void CreateRoom(int clientId, int maxPlayers, string serverName, bool isPublic, string serverData, bool useDirectConnect, string hostLocalIP, bool useNatPunch, int port)
{
LeaveRoom(clientId);
IPEndPoint hostIP = null;
Program.instance.NATConnections.TryGetValue(clientId, out hostIP);
Room room = new Room
{
hostId = clientId,
@ -232,8 +240,12 @@ namespace LightReflectiveMirror
isPublic = isPublic,
serverData = serverData,
clients = new List<int>(),
serverId = GetRandomServerID()
serverId = GetRandomServerID(),
hostIP = hostIP,
hostLocalIP = hostLocalIP,
supportsDirectConnect = hostIP == null ? false : useDirectConnect,
port = port,
useNATPunch = useNatPunch
};
rooms.Add(room);
@ -322,6 +334,7 @@ namespace LightReflectiveMirror
public enum OpCodes
{
Default = 0, RequestID = 1, JoinServer = 2, SendData = 3, GetID = 4, ServerJoined = 5, GetData = 6, CreateRoom = 7, ServerLeft = 8, PlayerDisconnected = 9, RoomCreated = 10,
LeaveRoom = 11, KickPlayer = 12, AuthenticationRequest = 13, AuthenticationResponse = 14, RequestServers = 15, ServerListReponse = 16, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19
LeaveRoom = 11, KickPlayer = 12, AuthenticationRequest = 13, AuthenticationResponse = 14, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19, RequestNATConnection = 20,
DirectConnectIP = 21
}
}

View file

@ -1,10 +1,10 @@
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Text;
using System.Net;
namespace LightReflectiveMirror
{
[Serializable]
[JsonObject(MemberSerialization.OptOut)]
public class Room
{
public int serverId;
@ -14,5 +14,15 @@ namespace LightReflectiveMirror
public bool isPublic;
public int maxPlayers;
public List<int> clients;
[JsonIgnore]
public bool supportsDirectConnect = false;
[JsonIgnore]
public IPEndPoint hostIP;
[JsonIgnore]
public string hostLocalIP;
[JsonIgnore]
public bool useNATPunch = false;
[JsonIgnore]
public int port;
}
}