using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Lidgren.Network; using Robust.Shared.Interfaces.Network; using Robust.Shared.Log; using Robust.Shared.Utility; namespace Robust.Shared.Network { public partial class NetManager { private CancellationTokenSource _cancelConnectTokenSource; private ClientConnectionState _clientConnectState; public ClientConnectionState ClientConnectState { get => _clientConnectState; private set { _clientConnectState = value; ClientConnectStateChanged?.Invoke(value); } } public event Action ClientConnectStateChanged; private readonly Dictionary tcs)> _awaitingStatusChange = new Dictionary)>(); private readonly Dictionary)> _awaitingData = new Dictionary) >(); /// public async void ClientConnect(string host, int port, string userNameRequest) { DebugTools.Assert(!IsServer, "Should never be called on the server."); if (ClientConnectState == ClientConnectionState.Connected) { throw new InvalidOperationException("The client is already connected to a server."); } if (ClientConnectState != ClientConnectionState.NotConnecting) { throw new InvalidOperationException("A connect attempt is already in progress. Cancel it first."); } _cancelConnectTokenSource = new CancellationTokenSource(); var mainCancelToken = _cancelConnectTokenSource.Token; ClientConnectState = ClientConnectionState.ResolvingHost; Logger.DebugS("net", "Attempting to connect to {0} port {1}", host, port); // Get list of potential IP addresses for the domain. var endPoints = await ResolveDnsAsync(host); if (mainCancelToken.IsCancellationRequested) { ClientConnectState = ClientConnectionState.NotConnecting; return; } if (endPoints == null) { OnConnectFailed($"Unable to resolve domain '{host}'"); ClientConnectState = ClientConnectionState.NotConnecting; return; } // Try to get an IPv6 and IPv4 address. var ipv6 = endPoints.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetworkV6); var ipv4 = endPoints.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork); if (ipv4 == null && ipv6 == null) { OnConnectFailed($"Domain '{host}' has no associated IP addresses"); ClientConnectState = ClientConnectionState.NotConnecting; return; } ClientConnectState = ClientConnectionState.EstablishingConnection; IPAddress first; IPAddress second = null; if (ipv6 != null) { // If there's an IPv6 address try it first then the IPv4. first = ipv6; second = ipv4; } else { first = ipv4; } Logger.DebugS("net", "First attempt IP address is {0}, second attempt {1}", first, second); NetPeer CreatePeerForIp(IPAddress address) { var config = _getBaseNetPeerConfig(); if (address.AddressFamily == AddressFamily.InterNetworkV6) { config.LocalAddress = IPAddress.IPv6Any; } else { config.LocalAddress = IPAddress.Any; } var peer = new NetPeer(config); peer.Start(); _netPeers.Add(peer); return peer; } // Create first peer. var firstPeer = CreatePeerForIp(first); var firstConnection = firstPeer.Connect(new IPEndPoint(first, port)); NetPeer secondPeer = null; NetConnection secondConnection = null; string secondReason = null; async Task AwaitNonInitStatusChange(NetConnection connection, CancellationToken cancellationToken) { NetConnectionStatus status; string reason; do { reason = await AwaitStatusChange(connection, cancellationToken); status = connection.Status; } while (status == NetConnectionStatus.InitiatedConnect); return reason; } async Task ConnectSecondDelayed(CancellationToken cancellationToken) { DebugTools.AssertNotNull(second); // Connecting via second peer is delayed by 25ms to give an advantage to IPv6, if it works. await Task.Delay(25, cancellationToken); if (cancellationToken.IsCancellationRequested) { return; } secondPeer = CreatePeerForIp(second); secondConnection = secondPeer.Connect(new IPEndPoint(second, port)); secondReason = await AwaitNonInitStatusChange(secondConnection, cancellationToken); } NetPeer winningPeer; NetConnection winningConnection; string firstReason = null; try { if (second != null) { // We have two addresses to try. var cancellation = CancellationTokenSource.CreateLinkedTokenSource(mainCancelToken); var firstPeerChanged = AwaitNonInitStatusChange(firstConnection, cancellation.Token); var secondPeerChanged = ConnectSecondDelayed(cancellation.Token); var firstChange = await Task.WhenAny(firstPeerChanged, secondPeerChanged); if (firstChange == firstPeerChanged) { Logger.DebugS("net", "First peer status changed."); // First peer responded first. if (firstConnection.Status == NetConnectionStatus.Connected) { // First peer won! Logger.DebugS("net", "First peer succeeded."); cancellation.Cancel(); if (secondPeer != null) { secondPeer.Shutdown("First connection attempt won."); _toCleanNetPeers.Add(secondPeer); } winningPeer = firstPeer; winningConnection = firstConnection; } else { // First peer failed, try the second one I guess. Logger.DebugS("net", "First peer failed."); firstPeer.Shutdown("You failed."); _toCleanNetPeers.Add(firstPeer); firstReason = firstPeerChanged.Result; await secondPeerChanged; winningPeer = secondPeer; winningConnection = secondConnection; } } else { if (secondConnection.Status == NetConnectionStatus.Connected) { // Second peer won! Logger.DebugS("net", "Second peer succeeded."); cancellation.Cancel(); firstPeer.Shutdown("Second connection attempt won."); _toCleanNetPeers.Add(firstPeer); winningPeer = secondPeer; winningConnection = secondConnection; } else { // First peer failed, try the second one I guess. Logger.DebugS("net", "Second peer failed."); secondPeer.Shutdown("You failed."); _toCleanNetPeers.Add(secondPeer); firstReason = await firstPeerChanged; winningPeer = firstPeer; winningConnection = firstConnection; } } } else { // Only one address to try. Pretty straight forward. firstReason = await AwaitNonInitStatusChange(firstConnection, mainCancelToken); winningPeer = firstPeer; winningConnection = firstConnection; } } catch (TaskCanceledException) { firstPeer.Shutdown("Cancelled"); _toCleanNetPeers.Add(firstPeer); if (secondPeer != null) { // ReSharper disable once PossibleNullReferenceException secondPeer.Shutdown("Cancelled"); _toCleanNetPeers.Add(secondPeer); } ClientConnectState = ClientConnectionState.NotConnecting; return; } // winningPeer can still be failed at this point. // If it is, neither succeeded. RIP. if (winningConnection.Status != NetConnectionStatus.Connected) { winningPeer.Shutdown("You failed"); _toCleanNetPeers.Add(winningPeer); OnConnectFailed(secondReason ?? firstReason); ClientConnectState = ClientConnectionState.NotConnecting; return; } ClientConnectState = ClientConnectionState.Handshake; // We're connected start handshaking. var userNameRequestMsg = winningPeer.CreateMessage(userNameRequest); winningPeer.SendMessage(userNameRequestMsg, winningConnection, NetDeliveryMethod.ReliableOrdered); try { // Await response. var response = await AwaitData(winningConnection, mainCancelToken); var receivedUsername = response.ReadString(); var channel = new NetChannel(this, winningConnection, new NetSessionId(receivedUsername)); _channels.Add(winningConnection, channel); var confirmConnectionMsg = winningPeer.CreateMessage("ok"); winningPeer.SendMessage(confirmConnectionMsg, winningConnection, NetDeliveryMethod.ReliableOrdered); } catch (TaskCanceledException) { winningPeer.Shutdown("Cancelled"); _toCleanNetPeers.Add(secondPeer); ClientConnectState = ClientConnectionState.NotConnecting; return; } catch (Exception e) { OnConnectFailed(e.Message); Logger.ErrorS("net", "Exception during handshake: {0}", e); winningPeer.Shutdown("Something happened."); _toCleanNetPeers.Add(secondPeer); ClientConnectState = ClientConnectionState.NotConnecting; return; } ClientConnectState = ClientConnectionState.Connected; Logger.DebugS("net", "Handshake completed, connection established."); } private Task AwaitStatusChange(NetConnection connection, CancellationToken cancellationToken = default) { if (_awaitingStatusChange.ContainsKey(connection)) { throw new InvalidOperationException(); } var tcs = new TaskCompletionSource(); CancellationTokenRegistration reg = default; if (cancellationToken != default) { reg = cancellationToken.Register(() => { _awaitingStatusChange.Remove(connection); tcs.TrySetCanceled(); }); } _awaitingStatusChange.Add(connection, (reg, tcs)); return tcs.Task; } private Task AwaitData(NetConnection connection, CancellationToken cancellationToken = default) { if (_awaitingData.ContainsKey(connection)) { throw new InvalidOperationException("Cannot await data twice."); } var tcs = new TaskCompletionSource(); CancellationTokenRegistration reg = default; if (cancellationToken != default) { reg = cancellationToken.Register(() => { _awaitingData.Remove(connection); tcs.TrySetCanceled(); }); } _awaitingData.Add(connection, (reg, tcs)); return tcs.Task; } public static async Task ResolveDnsAsync(string ipOrHost) { if (string.IsNullOrEmpty(ipOrHost)) { throw new ArgumentException("Supplied string must not be empty", nameof(ipOrHost)); } ipOrHost = ipOrHost.Trim(); if (IPAddress.TryParse(ipOrHost, out var ipAddress)) { if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) { return new[] {ipAddress}; } throw new ArgumentException("This method will not currently resolve other than IPv4 or IPv6 addresses"); } try { var entry = await Dns.GetHostEntryAsync(ipOrHost); return entry.AddressList; } catch (SocketException) { return null; } } } }