mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-06-28 00:16:23 +02:00

See merge request [ryubing/ryujinx!33](https://git.ryujinx.app/ryubing/ryujinx/-/merge_requests/33)
645 lines
20 KiB
C#
645 lines
20 KiB
C#
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 = [];
|
|
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([_connected, _error], FailureTimeout);
|
|
|
|
if (IsConnected)
|
|
{
|
|
SendAsync(_protocol.Encode(PacketId.Initialize, InitializeMemory));
|
|
}
|
|
|
|
return index == 0 && IsConnected;
|
|
}
|
|
|
|
private void UpdatePassphraseIfNeeded()
|
|
{
|
|
string passphrase = _config.MultiplayerLdnPassphrase ?? string.Empty;
|
|
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([_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(ReadOnlySpan<byte> versionString)
|
|
{
|
|
_gameVersion = versionString.ToArray();
|
|
|
|
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([_scan, _error], ScanTimeout);
|
|
}
|
|
|
|
if (index != 0)
|
|
{
|
|
// An error occurred or timeout. Write 0 games.
|
|
return [];
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|