| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645 |
- using Ryujinx.Common.Logging;
- using Ryujinx.Common.Utilities;
- using Ryujinx.HLE.HOS.Services.Ldn.Types;
- using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy;
- using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
- using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
- using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
- using Ryujinx.HLE.Utilities;
- using System;
- using System.Collections.Generic;
- using System.Net;
- using System.Net.NetworkInformation;
- using System.Net.Sockets;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- using TcpClient = NetCoreServer.TcpClient;
- namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
- {
- class LdnMasterProxyClient : TcpClient, INetworkClient, IProxyClient
- {
- public bool NeedsRealId => true;
- private static InitializeMessage InitializeMemory = new();
- private const int InactiveTimeout = 6000;
- private const int FailureTimeout = 4000;
- private const int ScanTimeout = 1000;
- private bool _useP2pProxy;
- private NetworkError _lastError;
- private readonly ManualResetEvent _connected = new(false);
- private readonly ManualResetEvent _error = new(false);
- private readonly ManualResetEvent _scan = new(false);
- private readonly ManualResetEvent _reject = new(false);
- private readonly AutoResetEvent _apConnected = new(false);
- private readonly RyuLdnProtocol _protocol;
- private readonly NetworkTimeout _timeout;
- private readonly List<NetworkInfo> _availableGames = new();
- private DisconnectReason _disconnectReason;
- private P2pProxyServer _hostedProxy;
- private P2pProxyClient _connectedProxy;
- private bool _networkConnected;
- private string _passphrase;
- private byte[] _gameVersion = new byte[0x10];
- private readonly HLEConfiguration _config;
- public event EventHandler<NetworkChangeEventArgs> NetworkChange;
- public ProxyConfig Config { get; private set; }
- public LdnMasterProxyClient(string address, int port, HLEConfiguration config) : base(address, port)
- {
- if (ProxyHelpers.SupportsNoDelay())
- {
- OptionNoDelay = true;
- }
- _protocol = new RyuLdnProtocol();
- _timeout = new NetworkTimeout(InactiveTimeout, TimeoutConnection);
- _protocol.Initialize += HandleInitialize;
- _protocol.Connected += HandleConnected;
- _protocol.Reject += HandleReject;
- _protocol.RejectReply += HandleRejectReply;
- _protocol.SyncNetwork += HandleSyncNetwork;
- _protocol.ProxyConfig += HandleProxyConfig;
- _protocol.Disconnected += HandleDisconnected;
- _protocol.ScanReply += HandleScanReply;
- _protocol.ScanReplyEnd += HandleScanReplyEnd;
- _protocol.ExternalProxy += HandleExternalProxy;
- _protocol.Ping += HandlePing;
- _protocol.NetworkError += HandleNetworkError;
- _config = config;
- _useP2pProxy = !config.MultiplayerDisableP2p;
- }
- private void TimeoutConnection()
- {
- _connected.Reset();
- DisconnectAsync();
- while (IsConnected)
- {
- Thread.Yield();
- }
- }
- private bool EnsureConnected()
- {
- if (IsConnected)
- {
- return true;
- }
- _error.Reset();
- ConnectAsync();
- int index = WaitHandle.WaitAny(new WaitHandle[] { _connected, _error }, FailureTimeout);
- if (IsConnected)
- {
- SendAsync(_protocol.Encode(PacketId.Initialize, InitializeMemory));
- }
- return index == 0 && IsConnected;
- }
- private void UpdatePassphraseIfNeeded()
- {
- string passphrase = _config.MultiplayerLdnPassphrase ?? "";
- if (passphrase != _passphrase)
- {
- _passphrase = passphrase;
- SendAsync(_protocol.Encode(PacketId.Passphrase, StringUtils.GetFixedLengthBytes(passphrase, 0x80, Encoding.UTF8)));
- }
- }
- protected override void OnConnected()
- {
- Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}");
- UpdatePassphraseIfNeeded();
- _connected.Set();
- }
- protected override void OnDisconnected()
- {
- Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}");
- _passphrase = null;
- _connected.Reset();
- if (_networkConnected)
- {
- DisconnectInternal();
- }
- }
- public void DisconnectAndStop()
- {
- _timeout.Dispose();
- DisconnectAsync();
- while (IsConnected)
- {
- Thread.Yield();
- }
- Dispose();
- }
- protected override void OnReceived(byte[] buffer, long offset, long size)
- {
- _protocol.Read(buffer, (int)offset, (int)size);
- }
- protected override void OnError(SocketError error)
- {
- Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}");
- _error.Set();
- }
- private void HandleInitialize(LdnHeader header, InitializeMessage initialize)
- {
- InitializeMemory = initialize;
- }
- private void HandleExternalProxy(LdnHeader header, ExternalProxyConfig config)
- {
- int length = config.AddressFamily switch
- {
- AddressFamily.InterNetwork => 4,
- AddressFamily.InterNetworkV6 => 16,
- _ => 0
- };
- if (length == 0)
- {
- return; // Invalid external proxy.
- }
- IPAddress address = new(config.ProxyIp.AsSpan()[..length].ToArray());
- P2pProxyClient proxy = new(address.ToString(), config.ProxyPort);
- _connectedProxy = proxy;
- bool success = proxy.PerformAuth(config);
- if (!success)
- {
- DisconnectInternal();
- }
- }
- private void HandlePing(LdnHeader header, PingMessage ping)
- {
- if (ping.Requester == 0) // Server requested.
- {
- // Send the ping message back.
- SendAsync(_protocol.Encode(PacketId.Ping, ping));
- }
- }
- private void HandleNetworkError(LdnHeader header, NetworkErrorMessage error)
- {
- if (error.Error == NetworkError.PortUnreachable)
- {
- _useP2pProxy = false;
- }
- else
- {
- _lastError = error.Error;
- }
- }
- private NetworkError ConsumeNetworkError()
- {
- NetworkError result = _lastError;
- _lastError = NetworkError.None;
- return result;
- }
- private void HandleSyncNetwork(LdnHeader header, NetworkInfo info)
- {
- NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true));
- }
- private void HandleConnected(LdnHeader header, NetworkInfo info)
- {
- _networkConnected = true;
- _disconnectReason = DisconnectReason.None;
- _apConnected.Set();
- NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true));
- }
- private void HandleDisconnected(LdnHeader header, DisconnectMessage message)
- {
- DisconnectInternal();
- }
- private void HandleReject(LdnHeader header, RejectRequest reject)
- {
- // When the client receives a Reject request, we have been rejected and will be disconnected shortly.
- _disconnectReason = reject.DisconnectReason;
- }
- private void HandleRejectReply(LdnHeader header)
- {
- _reject.Set();
- }
- private void HandleScanReply(LdnHeader header, NetworkInfo info)
- {
- _availableGames.Add(info);
- }
- private void HandleScanReplyEnd(LdnHeader obj)
- {
- _scan.Set();
- }
- private void DisconnectInternal()
- {
- if (_networkConnected)
- {
- _networkConnected = false;
- _hostedProxy?.Dispose();
- _hostedProxy = null;
- _connectedProxy?.Dispose();
- _connectedProxy = null;
- _apConnected.Reset();
- NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false, _disconnectReason));
- if (IsConnected)
- {
- _timeout.RefreshTimeout();
- }
- }
- }
- public void DisconnectNetwork()
- {
- if (_networkConnected)
- {
- SendAsync(_protocol.Encode(PacketId.Disconnect, new DisconnectMessage()));
- DisconnectInternal();
- }
- }
- public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId)
- {
- if (_networkConnected)
- {
- _reject.Reset();
- SendAsync(_protocol.Encode(PacketId.Reject, new RejectRequest(disconnectReason, nodeId)));
- int index = WaitHandle.WaitAny(new WaitHandle[] { _reject, _error }, InactiveTimeout);
- if (index == 0)
- {
- return (ConsumeNetworkError() != NetworkError.None) ? ResultCode.InvalidState : ResultCode.Success;
- }
- }
- return ResultCode.InvalidState;
- }
- public void SetAdvertiseData(byte[] data)
- {
- // TODO: validate we're the owner (the server will do this anyways tho)
- if (_networkConnected)
- {
- SendAsync(_protocol.Encode(PacketId.SetAdvertiseData, data));
- }
- }
- public void SetGameVersion(byte[] versionString)
- {
- _gameVersion = versionString;
- if (_gameVersion.Length < 0x10)
- {
- Array.Resize(ref _gameVersion, 0x10);
- }
- }
- public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy)
- {
- // TODO: validate we're the owner (the server will do this anyways tho)
- if (_networkConnected)
- {
- SendAsync(_protocol.Encode(PacketId.SetAcceptPolicy, new SetAcceptPolicyRequest
- {
- StationAcceptPolicy = acceptPolicy
- }));
- }
- }
- private void DisposeProxy()
- {
- _hostedProxy?.Dispose();
- _hostedProxy = null;
- }
- private void ConfigureAccessPoint(ref RyuNetworkConfig request)
- {
- _gameVersion.AsSpan().CopyTo(request.GameVersion.AsSpan());
- if (_useP2pProxy)
- {
- // Before sending the request, attempt to set up a proxy server.
- // This can be on a range of private ports, which can be exposed on a range of public
- // ports via UPnP. If any of this fails, we just fall back to using the master server.
- int i = 0;
- for (; i < P2pProxyServer.PrivatePortRange; i++)
- {
- _hostedProxy = new P2pProxyServer(this, (ushort)(P2pProxyServer.PrivatePortBase + i), _protocol);
- try
- {
- _hostedProxy.Start();
- break;
- }
- catch (SocketException e)
- {
- _hostedProxy.Dispose();
- _hostedProxy = null;
- if (e.SocketErrorCode != SocketError.AddressAlreadyInUse)
- {
- i = P2pProxyServer.PrivatePortRange; // Immediately fail.
- }
- }
- }
- bool openSuccess = i < P2pProxyServer.PrivatePortRange;
- if (openSuccess)
- {
- Task<ushort> natPunchResult = _hostedProxy.NatPunch();
- try
- {
- if (natPunchResult.Result != 0)
- {
- // Tell the server that we are hosting the proxy.
- request.ExternalProxyPort = natPunchResult.Result;
- }
- }
- catch (Exception) { }
- if (request.ExternalProxyPort == 0)
- {
- Logger.Warning?.Print(LogClass.ServiceLdn, "Failed to open a port with UPnP for P2P connection. Proxying through the master server instead. Expect higher latency.");
- _hostedProxy.Dispose();
- }
- else
- {
- Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}.");
- _hostedProxy.Start();
- (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface();
- unicastAddress.Address.GetAddressBytes().AsSpan().CopyTo(request.PrivateIp.AsSpan());
- request.InternalProxyPort = _hostedProxy.PrivatePort;
- request.AddressFamily = unicastAddress.Address.AddressFamily;
- }
- }
- else
- {
- Logger.Warning?.Print(LogClass.ServiceLdn, "Cannot create a P2P server. Proxying through the master server instead. Expect higher latency.");
- }
- }
- }
- private bool CreateNetworkCommon()
- {
- bool signalled = _apConnected.WaitOne(FailureTimeout);
- if (!_useP2pProxy && _hostedProxy != null)
- {
- Logger.Warning?.Print(LogClass.ServiceLdn, "Locally hosted proxy server was not externally reachable. Proxying through the master server instead. Expect higher latency.");
- DisposeProxy();
- }
- if (signalled && _connectedProxy != null)
- {
- _connectedProxy.EnsureProxyReady();
- Config = _connectedProxy.ProxyConfig;
- }
- else
- {
- DisposeProxy();
- }
- return signalled;
- }
- public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
- {
- _timeout.DisableTimeout();
- ConfigureAccessPoint(ref request.RyuNetworkConfig);
- if (!EnsureConnected())
- {
- DisposeProxy();
- return false;
- }
- UpdatePassphraseIfNeeded();
- SendAsync(_protocol.Encode(PacketId.CreateAccessPoint, request, advertiseData));
- // Send a network change event with dummy data immediately. Necessary to avoid crashes in some games
- NetworkChangeEventArgs networkChangeEvent = new(new NetworkInfo()
- {
- Common = new CommonNetworkInfo()
- {
- MacAddress = InitializeMemory.MacAddress,
- Channel = request.NetworkConfig.Channel,
- LinkLevel = 3,
- NetworkType = 2,
- Ssid = new Ssid()
- {
- Length = 32
- }
- },
- Ldn = new LdnNetworkInfo()
- {
- AdvertiseDataSize = (ushort)advertiseData.Length,
- AuthenticationId = 0,
- NodeCount = 1,
- NodeCountMax = request.NetworkConfig.NodeCountMax,
- SecurityMode = (ushort)request.SecurityConfig.SecurityMode
- }
- }, true);
- networkChangeEvent.Info.Ldn.Nodes[0] = new NodeInfo()
- {
- Ipv4Address = 175243265,
- IsConnected = 1,
- LocalCommunicationVersion = request.NetworkConfig.LocalCommunicationVersion,
- MacAddress = InitializeMemory.MacAddress,
- NodeId = 0,
- UserName = request.UserConfig.UserName
- };
- "12345678123456781234567812345678"u8.ToArray().CopyTo(networkChangeEvent.Info.Common.Ssid.Name.AsSpan());
- NetworkChange?.Invoke(this, networkChangeEvent);
- return CreateNetworkCommon();
- }
- public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
- {
- _timeout.DisableTimeout();
- ConfigureAccessPoint(ref request.RyuNetworkConfig);
- if (!EnsureConnected())
- {
- DisposeProxy();
- return false;
- }
- UpdatePassphraseIfNeeded();
- SendAsync(_protocol.Encode(PacketId.CreateAccessPointPrivate, request, advertiseData));
- return CreateNetworkCommon();
- }
- public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
- {
- if (!_networkConnected)
- {
- _timeout.RefreshTimeout();
- }
- _availableGames.Clear();
- int index = -1;
- if (EnsureConnected())
- {
- UpdatePassphraseIfNeeded();
- _scan.Reset();
- SendAsync(_protocol.Encode(PacketId.Scan, scanFilter));
- index = WaitHandle.WaitAny(new WaitHandle[] { _scan, _error }, ScanTimeout);
- }
- if (index != 0)
- {
- // An error occurred or timeout. Write 0 games.
- return Array.Empty<NetworkInfo>();
- }
- return _availableGames.ToArray();
- }
- private NetworkError ConnectCommon()
- {
- bool signalled = _apConnected.WaitOne(FailureTimeout);
- NetworkError error = ConsumeNetworkError();
- if (error != NetworkError.None)
- {
- return error;
- }
- if (signalled && _connectedProxy != null)
- {
- _connectedProxy.EnsureProxyReady();
- Config = _connectedProxy.ProxyConfig;
- }
- return signalled ? NetworkError.None : NetworkError.ConnectTimeout;
- }
- public NetworkError Connect(ConnectRequest request)
- {
- _timeout.DisableTimeout();
- if (!EnsureConnected())
- {
- return NetworkError.Unknown;
- }
- SendAsync(_protocol.Encode(PacketId.Connect, request));
- NetworkChangeEventArgs networkChangeEvent = new(new NetworkInfo()
- {
- Common = request.NetworkInfo.Common,
- Ldn = request.NetworkInfo.Ldn
- }, true);
- NetworkChange?.Invoke(this, networkChangeEvent);
- return ConnectCommon();
- }
- public NetworkError ConnectPrivate(ConnectPrivateRequest request)
- {
- _timeout.DisableTimeout();
- if (!EnsureConnected())
- {
- return NetworkError.Unknown;
- }
- SendAsync(_protocol.Encode(PacketId.ConnectPrivate, request));
- return ConnectCommon();
- }
- private void HandleProxyConfig(LdnHeader header, ProxyConfig config)
- {
- Config = config;
- SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol));
- }
- }
- }
|