using Gommon; using Humanizer; using NetCoreServer; using Open.Nat; using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy { class P2pProxyServer : TcpServer, IDisposable { public const ushort PrivatePortBase = 39990; public const int PrivatePortRange = 10; private const ushort PublicPortBase = 39990; private const int PublicPortRange = 10; private const ushort PortLeaseLength = 60; private const ushort PortLeaseRenew = 50; private const ushort AuthWaitSeconds = 1; private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); public ushort PrivatePort { get; } private ushort _publicPort; private bool _disposed; private readonly CancellationTokenSource _disposedCancellation = new(); private NatDevice _natDevice; private Mapping _portMapping; private readonly List _players = []; private readonly List _waitingTokens = []; private readonly AutoResetEvent _tokenEvent = new(false); private uint _broadcastAddress; private readonly LdnMasterProxyClient _master; private readonly RyuLdnProtocol _masterProtocol; private readonly RyuLdnProtocol _protocol; public P2pProxyServer(LdnMasterProxyClient master, ushort port, RyuLdnProtocol masterProtocol) : base(IPAddress.Any, port) { if (ProxyHelpers.SupportsNoDelay()) { OptionNoDelay = true; } PrivatePort = port; _master = master; _masterProtocol = masterProtocol; _masterProtocol.ExternalProxyState += HandleStateChange; _masterProtocol.ExternalProxyToken += HandleToken; _protocol = new RyuLdnProtocol(); } private void HandleToken(LdnHeader header, ExternalProxyToken token) { _lock.EnterWriteLock(); _waitingTokens.Add(token); _lock.ExitWriteLock(); _tokenEvent.Set(); } private void HandleStateChange(LdnHeader header, ExternalProxyConnectionState state) { if (!state.Connected) { _lock.EnterWriteLock(); _waitingTokens.RemoveAll(token => token.VirtualIp == state.IpAddress); _players.RemoveAll(player => { if (player.VirtualIpAddress == state.IpAddress) { player.DisconnectAndStop(); return true; } return false; }); _lock.ExitWriteLock(); } } public void Configure(ProxyConfig config) { _broadcastAddress = config.ProxyIp | (~config.ProxySubnetMask); } public async Task NatPunch() { NatDiscoverer discoverer = new(); CancellationTokenSource cts = new(2500); NatDevice device; try { device = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, cts); } catch (NatDeviceNotFoundException) { return 0; } _publicPort = PublicPortBase; for (int i = 0; i < PublicPortRange; i++) { try { _portMapping = new Mapping(Protocol.Tcp, PrivatePort, _publicPort, PortLeaseLength, "Ryujinx Local Multiplayer"); await device.CreatePortMapAsync(_portMapping); break; } catch (MappingException) { _publicPort++; } catch (Exception) { return 0; } if (i == PublicPortRange - 1) { _publicPort = 0; } } if (_publicPort != 0) { _ = Executor.ExecuteAfterDelayAsync( PortLeaseRenew.Seconds(), _disposedCancellation.Token, RefreshLease); } _natDevice = device; return _publicPort; } // Proxy handlers private void RouteMessage(P2pProxySession sender, ref ProxyInfo info, Action action) { if (info.SourceIpV4 == 0) { // If they sent from a connection bound on 0.0.0.0, make others see it as them. info.SourceIpV4 = sender.VirtualIpAddress; } else if (info.SourceIpV4 != sender.VirtualIpAddress) { // Can't pretend to be somebody else. return; } uint destIp = info.DestIpV4; if (destIp == 0xc0a800ff) { destIp = _broadcastAddress; } bool isBroadcast = destIp == _broadcastAddress; _lock.EnterReadLock(); if (isBroadcast) { _players.ForEach(player => { action(player); }); } else { P2pProxySession target = _players.FirstOrDefault(player => player.VirtualIpAddress == destIp); if (target != null) { action(target); } } _lock.ExitReadLock(); } public void HandleProxyDisconnect(P2pProxySession sender, LdnHeader header, ProxyDisconnectMessage message) { RouteMessage(sender, ref message.Info, (target) => { target.SendAsync(sender.Protocol.Encode(PacketId.ProxyDisconnect, message)); }); } public void HandleProxyData(P2pProxySession sender, LdnHeader header, ProxyDataHeader message, byte[] data) { RouteMessage(sender, ref message.Info, (target) => { target.SendAsync(sender.Protocol.Encode(PacketId.ProxyData, message, data)); }); } public void HandleProxyConnectReply(P2pProxySession sender, LdnHeader header, ProxyConnectResponse message) { RouteMessage(sender, ref message.Info, (target) => { target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnectReply, message)); }); } public void HandleProxyConnect(P2pProxySession sender, LdnHeader header, ProxyConnectRequest message) { RouteMessage(sender, ref message.Info, (target) => { target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnect, message)); }); } // End proxy handlers private async Task RefreshLease() { if (_disposed || _natDevice == null) { return; } try { await _natDevice.CreatePortMapAsync(_portMapping); } catch (Exception) { } _ = Executor.ExecuteAfterDelayAsync( PortLeaseRenew.Milliseconds(), _disposedCancellation.Token, RefreshLease); } public bool TryRegisterUser(P2pProxySession session, ExternalProxyConfig config) { _lock.EnterWriteLock(); // Attempt to find matching configuration. If we don't find one, wait for a bit and try again. // Woken by new tokens coming in from the master server. IPAddress address = (session.Socket.RemoteEndPoint as IPEndPoint).Address; byte[] addressBytes = ProxyHelpers.AddressTo16Byte(address); long time; long endTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency * AuthWaitSeconds; do { for (int i = 0; i < _waitingTokens.Count; i++) { ExternalProxyToken waitToken = _waitingTokens[i]; // Allow any client that has a private IP to connect. (indicated by the server as all 0 in the token) bool isPrivate = waitToken.PhysicalIp.AsSpan().SequenceEqual(new byte[16]); bool ipEqual = isPrivate || waitToken.AddressFamily == address.AddressFamily && waitToken.PhysicalIp.AsSpan().SequenceEqual(addressBytes); if (ipEqual && waitToken.Token.AsSpan().SequenceEqual(config.Token.AsSpan())) { // This is a match. _waitingTokens.RemoveAt(i); session.SetIpv4(waitToken.VirtualIp); ProxyConfig pconfig = new() { ProxyIp = session.VirtualIpAddress, ProxySubnetMask = 0xFFFF0000 // TODO: Use from server. }; if (_players.Count == 0) { Configure(pconfig); } _players.Add(session); session.SendAsync(_protocol.Encode(PacketId.ProxyConfig, pconfig)); _lock.ExitWriteLock(); return true; } } // Couldn't find the token. // It may not have arrived yet, so wait for one to arrive. _lock.ExitWriteLock(); time = Stopwatch.GetTimestamp(); int remainingMs = (int)((endTime - time) / (Stopwatch.Frequency / 1000)); if (remainingMs < 0) { remainingMs = 0; } _tokenEvent.WaitOne(remainingMs); _lock.EnterWriteLock(); } while (time < endTime); _lock.ExitWriteLock(); return false; } public void DisconnectProxyClient(P2pProxySession session) { _lock.EnterWriteLock(); bool removed = _players.Remove(session); if (removed) { _master.SendAsync(_masterProtocol.Encode(PacketId.ExternalProxyState, new ExternalProxyConnectionState { IpAddress = session.VirtualIpAddress, Connected = false })); } _lock.ExitWriteLock(); } public new void Dispose() { base.Dispose(); _disposed = true; _disposedCancellation.Cancel(); try { Task delete = _natDevice?.DeletePortMapAsync(new Mapping(Protocol.Tcp, PrivatePort, _publicPort, 60, "Ryujinx Local Multiplayer")); // Just absorb any exceptions. delete?.ContinueWith((task) => { }); } catch (Exception) { // Fail silently. } } protected override TcpSession CreateSession() { return new P2pProxySession(this); } protected override void OnError(SocketError error) { Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP server caught an error with code {error}"); } } }