Implement IPv6 support for server and direct connect

This commit is contained in:
jrb0001
2020-01-06 22:19:52 +01:00
committed by reaperrr
parent bd1a936c7a
commit bf397591f9
16 changed files with 328 additions and 118 deletions

View File

@@ -61,13 +61,13 @@ namespace OpenRA
public static event Action OnShellmapLoaded = () => { };
public static OrderManager JoinServer(string host, int port, string password, bool recordReplay = true)
public static OrderManager JoinServer(ConnectionTarget endpoint, string password, bool recordReplay = true)
{
var connection = new NetworkConnection(host, port);
var connection = new NetworkConnection(endpoint);
if (recordReplay)
connection.StartRecording(() => { return TimestampedFilename(); });
var om = new OrderManager(host, port, password, connection);
var om = new OrderManager(endpoint, password, connection);
JoinInner(om);
return om;
}
@@ -88,12 +88,12 @@ namespace OpenRA
public static void JoinReplay(string replayFile)
{
JoinInner(new OrderManager("<no server>", -1, "", new ReplayConnection(replayFile)));
JoinInner(new OrderManager(new ConnectionTarget(), "", new ReplayConnection(replayFile)));
}
static void JoinLocal()
{
JoinInner(new OrderManager("<no server>", -1, "", new EchoConnection()));
JoinInner(new OrderManager(new ConnectionTarget(), "", new EchoConnection()));
}
// More accurate replacement for Environment.TickCount
@@ -104,14 +104,14 @@ namespace OpenRA
public static int NetFrameNumber { get { return OrderManager.NetFrameNumber; } }
public static int LocalTick { get { return OrderManager.LocalFrameNumber; } }
public static event Action<string, int> OnRemoteDirectConnect = (a, b) => { };
public static event Action<ConnectionTarget> OnRemoteDirectConnect = _ => { };
public static event Action<OrderManager> ConnectionStateChanged = _ => { };
static ConnectionState lastConnectionState = ConnectionState.PreConnecting;
public static int LocalClientId { get { return OrderManager.Connection.LocalClientId; } }
public static void RemoteDirectConnect(string host, int port)
public static void RemoteDirectConnect(ConnectionTarget endpoint)
{
OnRemoteDirectConnect(host, port);
OnRemoteDirectConnect(endpoint);
}
// Hacky workaround for orderManager visibility
@@ -233,7 +233,7 @@ namespace OpenRA
LobbyInfoChanged += lobbyReady;
om = JoinServer(IPAddress.Loopback.ToString(), CreateLocalServer(mapUID), "");
om = JoinServer(CreateLocalServer(mapUID), "");
}
public static bool IsHost
@@ -301,6 +301,7 @@ namespace OpenRA
Log.AddChannel("graphics", "graphics.log");
Log.AddChannel("geoip", "geoip.log");
Log.AddChannel("nat", "nat.log");
Log.AddChannel("client", "client.log");
var platforms = new[] { Settings.Game.Platform, "Default", null };
foreach (var p in platforms)
@@ -384,7 +385,7 @@ namespace OpenRA
LobbyInfoChanged = () => { };
ConnectionStateChanged = om => { };
BeforeGameStart = () => { };
OnRemoteDirectConnect = (a, b) => { };
OnRemoteDirectConnect = endpoint => { };
delayedActions = new ActionQueue();
Ui.ResetAll();
@@ -898,12 +899,19 @@ namespace OpenRA
return ModData.ObjectCreator.CreateObject<T>(name);
}
public static void CreateServer(ServerSettings settings)
public static ConnectionTarget CreateServer(ServerSettings settings)
{
server = new Server.Server(new IPEndPoint(IPAddress.Any, settings.ListenPort), settings, ModData, ServerType.Multiplayer);
var endpoints = new List<IPEndPoint>
{
new IPEndPoint(IPAddress.IPv6Any, settings.ListenPort),
new IPEndPoint(IPAddress.Any, settings.ListenPort)
};
server = new Server.Server(endpoints, settings, ModData, ServerType.Multiplayer);
return server.GetEndpointForLocalConnection();
}
public static int CreateLocalServer(string map)
public static ConnectionTarget CreateLocalServer(string map)
{
var settings = new ServerSettings()
{
@@ -912,9 +920,14 @@ namespace OpenRA
AdvertiseOnline = false
};
server = new Server.Server(new IPEndPoint(IPAddress.Loopback, 0), settings, ModData, ServerType.Local);
var endpoints = new List<IPEndPoint>
{
new IPEndPoint(IPAddress.IPv6Loopback, 0),
new IPEndPoint(IPAddress.Loopback, 0)
};
server = new Server.Server(endpoints, settings, ModData, ServerType.Local);
return server.Port;
return server.GetEndpointForLocalConnection();
}
public static bool IsCurrentWorld(World world)

View File

@@ -10,8 +10,11 @@
#endregion
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using OpenRA.Server;
@@ -30,12 +33,63 @@ namespace OpenRA.Network
{
int LocalClientId { get; }
ConnectionState ConnectionState { get; }
IPEndPoint EndPoint { get; }
string ErrorMessage { get; }
void Send(int frame, List<byte[]> orders);
void SendImmediate(IEnumerable<byte[]> orders);
void SendSync(int frame, byte[] syncData);
void Receive(Action<int, byte[]> packetFn);
}
public class ConnectionTarget
{
readonly DnsEndPoint[] endpoints;
public ConnectionTarget()
{
endpoints = new[] { new DnsEndPoint("invalid", 0) };
}
public ConnectionTarget(string host, int port)
{
endpoints = new[] { new DnsEndPoint(host, port) };
}
public ConnectionTarget(IEnumerable<DnsEndPoint> endpoints)
{
this.endpoints = endpoints.ToArray();
if (this.endpoints.Length == 0)
{
throw new ArgumentException("ConnectionTarget must have at least one address.");
}
}
public IEnumerable<IPEndPoint> GetConnectEndPoints()
{
return endpoints
.SelectMany(e =>
{
try
{
return Dns.GetHostAddresses(e.Host)
.Select(a => new IPEndPoint(a, e.Port));
}
catch (Exception)
{
return Enumerable.Empty<IPEndPoint>();
}
})
.ToList();
}
public override string ToString()
{
return endpoints
.Select(e => "{0}:{1}".F(e.Host, e.Port))
.JoinWith("/");
}
}
class EchoConnection : IConnection
{
protected struct ReceivedPacket
@@ -57,6 +111,16 @@ namespace OpenRA.Network
get { return ConnectionState.PreConnecting; }
}
public virtual IPEndPoint EndPoint
{
get { throw new NotSupportedException("An echo connection doesn't have an endpoint"); }
}
public virtual string ErrorMessage
{
get { return null; }
}
public virtual void Send(int frame, List<byte[]> orders)
{
var ms = new MemoryStream();
@@ -138,35 +202,100 @@ namespace OpenRA.Network
sealed class NetworkConnection : EchoConnection
{
readonly TcpClient tcp;
readonly ConnectionTarget target;
TcpClient tcp;
IPEndPoint endpoint;
readonly List<byte[]> queuedSyncPackets = new List<byte[]>();
volatile ConnectionState connectionState = ConnectionState.Connecting;
volatile int clientId;
bool disposed;
string errorMessage;
public NetworkConnection(string host, int port)
public override IPEndPoint EndPoint { get { return endpoint; } }
public override string ErrorMessage { get { return errorMessage; } }
public NetworkConnection(ConnectionTarget target)
{
try
this.target = target;
new Thread(NetworkConnectionConnect)
{
tcp = new TcpClient(host, port) { NoDelay = true };
Name = "{0} (connect to {1})".F(GetType().Name, target),
IsBackground = true
}.Start();
}
void NetworkConnectionConnect()
{
var queue = new BlockingCollection<TcpClient>();
var atLeastOneEndpoint = false;
foreach (var endpoint in target.GetConnectEndPoints())
{
atLeastOneEndpoint = true;
new Thread(() =>
{
try
{
var client = new TcpClient(endpoint.AddressFamily) { NoDelay = true };
client.Connect(endpoint.Address, endpoint.Port);
try
{
queue.Add(client);
}
catch (InvalidOperationException)
{
// Another connection was faster, close this one.
client.Close();
}
}
catch (Exception ex)
{
errorMessage = "Failed to connect to {0}".F(endpoint);
Log.Write("client", "Failed to connect to {0}: {1}".F(endpoint, ex.Message));
}
})
{
Name = "{0} (connect to {1})".F(GetType().Name, endpoint),
IsBackground = true
}.Start();
}
if (!atLeastOneEndpoint)
{
errorMessage = "Failed to resolve addresses for {0}".F(target);
connectionState = ConnectionState.NotConnected;
}
// Wait up to 5s for a successful connection. This should hopefully be enough because such high latency makes the game unplayable anyway.
else if (queue.TryTake(out tcp, 5000))
{
// Copy endpoint here to have it even after getting disconnected.
endpoint = (IPEndPoint)tcp.Client.RemoteEndPoint;
new Thread(NetworkConnectionReceive)
{
Name = GetType().Name + " " + host + ":" + port,
Name = "{0} (receive from {1})".F(GetType().Name, tcp.Client.RemoteEndPoint),
IsBackground = true
}.Start(tcp.GetStream());
}.Start();
}
catch
else
{
connectionState = ConnectionState.NotConnected;
}
// Close all unneeded connections in the queue and make sure new ones are closed on the connect thread.
queue.CompleteAdding();
foreach (var client in queue)
client.Close();
}
void NetworkConnectionReceive(object networkStreamObject)
void NetworkConnectionReceive()
{
try
{
var networkStream = (NetworkStream)networkStreamObject;
var reader = new BinaryReader(networkStream);
var reader = new BinaryReader(tcp.GetStream());
var handshakeProtocol = reader.ReadInt32();
if (handshakeProtocol != ProtocolVersion.Handshake)
@@ -187,7 +316,11 @@ namespace OpenRA.Network
AddPacket(new ReceivedPacket { FromClient = client, Data = buf });
}
}
catch { }
catch (Exception ex)
{
errorMessage = "Connection to {0} failed".F(endpoint);
Log.Write("client", "Connection to {0} failed: {1}".F(endpoint, ex.Message));
}
finally
{
connectionState = ConnectionState.NotConnected;

View File

@@ -28,11 +28,10 @@ namespace OpenRA.Network
public Session.Client LocalClient { get { return LobbyInfo.ClientWithIndex(Connection.LocalClientId); } }
public World World;
public readonly string Host;
public readonly int Port;
public readonly ConnectionTarget Endpoint;
public readonly string Password = "";
public string ServerError = "Server is not responding";
public string ServerError = null;
public bool AuthenticationFailed = false;
public ExternalMod ServerExternalMod = null;
@@ -80,10 +79,9 @@ namespace OpenRA.Network
Connection.Send(i, new List<byte[]>());
}
public OrderManager(string host, int port, string password, IConnection conn)
public OrderManager(ConnectionTarget endpoint, string password, IConnection conn)
{
Host = host;
Port = port;
Endpoint = endpoint;
Password = password;
Connection = conn;
syncReport = new SyncReport(this);

View File

@@ -12,6 +12,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using OpenRA.FileFormats;
using OpenRA.Primitives;
@@ -32,6 +33,13 @@ namespace OpenRA.Network
public int LocalClientId { get { return -1; } }
public ConnectionState ConnectionState { get { return ConnectionState.Connected; } }
public IPEndPoint EndPoint
{
get { throw new NotSupportedException("A replay connection doesn't have an endpoint"); }
}
public string ErrorMessage { get { return null; } }
public readonly int TickCount;
public readonly int FinalGameTick;
public readonly bool IsValid;

View File

@@ -18,7 +18,6 @@ using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using OpenRA.Graphics;
using OpenRA.Network;
using OpenRA.Primitives;
using OpenRA.Support;
@@ -43,8 +42,6 @@ namespace OpenRA.Server
{
public readonly string TwoHumansRequiredText = "This server requires at least two human players to start a match.";
public readonly IPAddress Ip;
public readonly int Port;
public readonly MersenneTwister Random = new MersenneTwister();
public readonly ServerType Type;
@@ -64,7 +61,7 @@ namespace OpenRA.Server
public GameSave GameSave = null;
readonly int randomSeed;
readonly TcpListener listener;
readonly List<TcpListener> listeners = new List<TcpListener>();
readonly TypeDictionary serverTraits = new TypeDictionary();
readonly PlayerDatabase playerDatabase;
@@ -129,15 +126,43 @@ namespace OpenRA.Server
t.GameEnded(this);
}
public Server(IPEndPoint endpoint, ServerSettings settings, ModData modData, ServerType type)
public Server(List<IPEndPoint> endpoints, ServerSettings settings, ModData modData, ServerType type)
{
Log.AddChannel("server", "server.log", true);
listener = new TcpListener(endpoint);
listener.Start();
var localEndpoint = (IPEndPoint)listener.LocalEndpoint;
Ip = localEndpoint.Address;
Port = localEndpoint.Port;
SocketException lastException = null;
var checkReadServer = new List<Socket>();
foreach (var endpoint in endpoints)
{
var listener = new TcpListener(endpoint);
try
{
try
{
listener.Server.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 1);
}
catch (Exception ex)
{
if (ex is SocketException || ex is ArgumentException)
Log.Write("server", "Failed to set socket option on {0}: {1}", endpoint.ToString(), ex.Message);
else
throw;
}
listener.Start();
listeners.Add(listener);
checkReadServer.Add(listener.Server);
}
catch (SocketException ex)
{
lastException = ex;
Log.Write("server", "Failed to listen on {0}: {1}", endpoint.ToString(), ex.Message);
}
}
if (listeners.Count == 0)
throw lastException;
Type = type;
Settings = settings;
@@ -186,7 +211,7 @@ namespace OpenRA.Server
{
var checkRead = new List<Socket>();
if (State == ServerState.WaitingPlayers)
checkRead.Add(listener.Server);
checkRead.AddRange(checkReadServer);
checkRead.AddRange(Conns.Select(c => c.Socket));
checkRead.AddRange(PreConns.Select(c => c.Socket));
@@ -205,9 +230,10 @@ namespace OpenRA.Server
foreach (var s in checkRead)
{
if (s == listener.Server)
var serverIndex = checkReadServer.IndexOf(s);
if (serverIndex >= 0)
{
AcceptConnection();
AcceptConnection(listeners[serverIndex]);
continue;
}
@@ -246,9 +272,14 @@ namespace OpenRA.Server
PreConns.Clear();
Conns.Clear();
try { listener.Stop(); }
catch { }
}) { IsBackground = true }.Start();
foreach (var listener in listeners)
{
try { listener.Stop(); }
catch { }
}
})
{ IsBackground = true }.Start();
}
int nextPlayerIndex;
@@ -257,7 +288,7 @@ namespace OpenRA.Server
return nextPlayerIndex++;
}
void AcceptConnection()
void AcceptConnection(TcpListener listener)
{
Socket newSocket;
@@ -956,7 +987,8 @@ namespace OpenRA.Server
public void StartGame()
{
listener.Stop();
foreach (var listener in listeners)
listener.Stop();
Console.WriteLine("[{0}] Game started", DateTime.Now.ToString(Settings.TimestampFormat));
@@ -1018,5 +1050,22 @@ namespace OpenRA.Server
});
}
}
public ConnectionTarget GetEndpointForLocalConnection()
{
var endpoints = new List<DnsEndPoint>();
foreach (var listener in listeners)
{
var endpoint = (IPEndPoint)listener.LocalEndpoint;
if (IPAddress.IPv6Any.Equals(endpoint.Address))
endpoints.Add(new DnsEndPoint(IPAddress.IPv6Loopback.ToString(), endpoint.Port));
else if (IPAddress.Any.Equals(endpoint.Address))
endpoints.Add(new DnsEndPoint(IPAddress.Loopback.ToString(), endpoint.Port));
else
endpoints.Add(new DnsEndPoint(endpoint.Address.ToString(), endpoint.Port));
}
return new ConnectionTarget(endpoints);
}
}
}

View File

@@ -9,6 +9,9 @@
*/
#endregion
using System;
using OpenRA.Network;
namespace OpenRA
{
public class LaunchArguments
@@ -38,17 +41,28 @@ namespace OpenRA
FieldLoader.LoadField(this, f.Name, args.GetValue("Launch" + "." + f.Name, ""));
}
public string GetConnectAddress()
public ConnectionTarget GetConnectEndPoint()
{
var connect = string.Empty;
try
{
Uri uri;
if (!string.IsNullOrEmpty(URI))
uri = new Uri(URI);
else if (!string.IsNullOrEmpty(Connect))
uri = new Uri("tcp://" + Connect);
else
return null;
if (!string.IsNullOrEmpty(Connect))
connect = Connect;
if (!string.IsNullOrEmpty(URI))
connect = URI.Substring(URI.IndexOf("://", System.StringComparison.Ordinal) + 3).TrimEnd('/');
return connect;
if (uri.IsAbsoluteUri)
return new ConnectionTarget(uri.Host, uri.Port);
else
return null;
}
catch (Exception ex)
{
Log.Write("client", "Failed to parse Launch.URI or Launch.Connect: {0}", ex.Message);
return null;
}
}
}
}