diff --git a/Directory.Packages.props b/Directory.Packages.props index 4fd079af6..009430f92 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,6 +24,7 @@ + diff --git a/README.md b/README.md index 56333278f..b2a6646f5 100644 --- a/README.md +++ b/README.md @@ -141,4 +141,5 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY - [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system. - [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation. +- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes. - [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation. diff --git a/distribution/legal/THIRDPARTY.md b/distribution/legal/THIRDPARTY.md index b0bd5a690..5caa03771 100644 --- a/distribution/legal/THIRDPARTY.md +++ b/distribution/legal/THIRDPARTY.md @@ -710,4 +710,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index c473cf562..cd066efba 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -190,6 +190,7 @@ namespace Ryujinx.Ava ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; + ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState; @@ -408,6 +409,11 @@ namespace Ryujinx.Ava }); } + private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs e) + { + Device.Configuration.EnableInternetAccess = e.NewValue; + } + private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs e) { Device.Configuration.MultiplayerLanInterfaceId = e.NewValue; diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index a67b796bd..62aac1227 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -650,7 +650,7 @@ "UserEditorTitle": "Edit User", "UserEditorTitleCreate": "Create User", "SettingsTabNetworkInterface": "Network Interface:", - "NetworkInterfaceTooltip": "The network interface used for LAN features", + "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features", "NetworkInterfaceDefault": "Default", "PackagingShaders": "Packaging Shaders", "AboutChangelogButton": "View Changelog on GitHub", diff --git a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs index 167429433..05108716d 100644 --- a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs +++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs @@ -3,5 +3,6 @@ public enum MultiplayerMode { Disabled, + LdnMitm, } } diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs index 78fb342b1..3b64a28f5 100644 --- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs +++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs @@ -74,5 +74,10 @@ namespace Ryujinx.Common.Utilities { return ConvertIpv4Address(IPAddress.Parse(ipAddress)); } + + public static IPAddress ConvertUint(uint ipAddress) + { + return new IPAddress(new byte[] { (byte)((ipAddress >> 24) & 0xFF), (byte)((ipAddress >> 16) & 0xFF), (byte)((ipAddress >> 8) & 0xFF), (byte)(ipAddress & 0xFF) }); + } } } diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index b1ba11b59..d52f1815a 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -101,7 +101,7 @@ namespace Ryujinx.HLE /// /// Control if the guest application should be told that there is a Internet connection available. /// - internal readonly bool EnableInternetAccess; + public bool EnableInternetAccess { internal get; set; } /// /// Control LibHac's integrity check level. diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs new file mode 100644 index 000000000..80ea2c9d7 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn +{ + static class LdnConst + { + public const int SsidLengthMax = 0x20; + public const int AdvertiseDataSizeMax = 0x180; + public const int UserNameBytesMax = 0x20; + public const int NodeCountMax = 8; + public const int StationCountMax = NodeCountMax - 1; + public const int PassphraseLengthMax = 0x40; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs index 4b7241c43..5fb2aca05 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs index c57a7dc45..9d5477931 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs index f33ceaebe..0461e783e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types @@ -48,7 +48,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Types { result[i].Reserved = new Array7(); - if (i < 8) + if (i < LdnConst.NodeCountMax) { result[i].State = array[i].State; array[i].State = NodeLatestUpdateFlags.None; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs index 85a19a875..5939a1394 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs index 72db4d41a..764862508 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs index 1401f5214..3820f936e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs index 07bbbeda3..78ebcac82 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs @@ -1,7 +1,6 @@ using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator @@ -30,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _parent.NetworkClient.NetworkChange -= NetworkChanged; } - private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e) + private void NetworkChanged(object sender, NetworkChangeEventArgs e) { LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs similarity index 81% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs index ff342d27c..81825e977 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs @@ -1,12 +1,13 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { interface INetworkClient : IDisposable { + bool NeedsRealId { get; } + event EventHandler NetworkChange; void DisconnectNetwork(); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 29cc0e1b9..8c6ea66f7 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -8,7 +8,7 @@ using Ryujinx.Cpu; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; @@ -395,7 +395,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { - if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1) + if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId) { // TODO: Call nn::arp::GetApplicationControlProperty here when implemented. ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; @@ -546,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment? NetworkConfig networkConfig = context.RequestData.ReadStruct(); - if (networkConfig.IntentId.LocalCommunicationId == -1) + if (networkConfig.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId) { // TODO: Call nn::arp::GetApplicationControlProperty here when implemented. ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; @@ -555,7 +555,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId); - if (!isLocalCommunicationIdValid) + if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { return ResultCode.InvalidObject; } @@ -568,13 +568,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel); securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode); - if (networkConfig.NodeCountMax <= 8) + if (networkConfig.NodeCountMax <= LdnConst.NodeCountMax) { if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0) { if (securityConfig.SecurityMode <= SecurityMode.Retail) { - if (securityConfig.Passphrase.Length <= 0x40) + if (securityConfig.Passphrase.Length <= LdnConst.PassphraseLengthMax) { if (_state == NetworkState.AccessPoint) { @@ -678,7 +678,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator return _nifmResultCode; } - if (bufferSize == 0 || bufferSize > 0x180) + if (bufferSize == 0 || bufferSize > LdnConst.AdvertiseDataSizeMax) { return ResultCode.InvalidArgument; } @@ -848,10 +848,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator context.Memory.Read(bufferPosition, networkInfoBytes); - networkInfo = MemoryMarshal.Cast(networkInfoBytes)[0]; + networkInfo = MemoryMarshal.Read(networkInfoBytes); } - if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1) + if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId) { // TODO: Call nn::arp::GetApplicationControlProperty here when implemented. ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; @@ -860,7 +860,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId); - if (!isLocalCommunicationIdValid) + if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { return ResultCode.InvalidObject; } @@ -1061,10 +1061,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) { MultiplayerMode mode = context.Device.Configuration.MultiplayerMode; + + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initializing with multiplayer mode: {mode}"); + switch (mode) { + case MultiplayerMode.LdnMitm: + NetworkClient = new LdnMitmClient(context.Device.Configuration); + break; case MultiplayerMode.Disabled: - NetworkClient = new DisabledLdnClient(); + NetworkClient = new LdnDisabledClient(); break; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs similarity index 87% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs index 75a1e35ff..e5340b4e9 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs @@ -1,12 +1,13 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { - class DisabledLdnClient : INetworkClient + class LdnDisabledClient : INetworkClient { + public bool NeedsRealId => true; + public event EventHandler NetworkChange; public NetworkError Connect(ConnectRequest request) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs new file mode 100644 index 000000000..8cfd77acb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs @@ -0,0 +1,611 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm +{ + internal class LanDiscovery : IDisposable + { + private const int DefaultPort = 11452; + private const ushort CommonChannel = 6; + private const byte CommonLinkLevel = 3; + private const byte CommonNetworkType = 2; + + private const int FailureTimeout = 4000; + + private readonly LdnMitmClient _parent; + private readonly LanProtocol _protocol; + private bool _initialized; + private readonly Ssid _fakeSsid; + private ILdnTcpSocket _tcp; + private LdnProxyUdpServer _udp, _udp2; + private readonly List _stations = new(); + private readonly object _lock = new(); + + private readonly AutoResetEvent _apConnected = new(false); + + internal readonly IPAddress LocalAddr; + internal readonly IPAddress LocalBroadcastAddr; + internal NetworkInfo NetworkInfo; + + public bool IsHost => _tcp is LdnProxyTcpServer; + + private readonly Random _random = new(); + + // NOTE: Credit to https://stackoverflow.com/a/39338188 + private static IPAddress GetBroadcastAddress(IPAddress address, IPAddress mask) + { + uint ipAddress = BitConverter.ToUInt32(address.GetAddressBytes(), 0); + uint ipMaskV4 = BitConverter.ToUInt32(mask.GetAddressBytes(), 0); + uint broadCastIpAddress = ipAddress | ~ipMaskV4; + + return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); + } + + private static NetworkInfo GetEmptyNetworkInfo() + { + NetworkInfo networkInfo = new() + { + NetworkId = new NetworkId + { + SessionId = new Array16(), + }, + Common = new CommonNetworkInfo + { + MacAddress = new Array6(), + Ssid = new Ssid + { + Name = new Array33(), + }, + }, + Ldn = new LdnNetworkInfo + { + NodeCountMax = LdnConst.NodeCountMax, + SecurityParameter = new Array16(), + Nodes = new Array8(), + AdvertiseData = new Array384(), + Reserved4 = new Array140(), + }, + }; + + for (int i = 0; i < LdnConst.NodeCountMax; i++) + { + networkInfo.Ldn.Nodes[i] = new NodeInfo + { + MacAddress = new Array6(), + UserName = new Array33(), + Reserved2 = new Array16(), + }; + } + + return networkInfo; + } + + public LanDiscovery(LdnMitmClient parent, IPAddress ipAddress, IPAddress ipv4Mask) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initialize LanDiscovery using IP: {ipAddress}"); + + _parent = parent; + LocalAddr = ipAddress; + LocalBroadcastAddr = GetBroadcastAddress(ipAddress, ipv4Mask); + + _fakeSsid = new Ssid + { + Length = LdnConst.SsidLengthMax, + }; + _random.NextBytes(_fakeSsid.Name.AsSpan()[..32]); + + _protocol = new LanProtocol(this); + _protocol.Accept += OnConnect; + _protocol.SyncNetwork += OnSyncNetwork; + _protocol.DisconnectStation += DisconnectStation; + + NetworkInfo = GetEmptyNetworkInfo(); + + ResetStations(); + + if (!InitUdp()) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Initialize: InitUdp failed."); + + return; + } + + _initialized = true; + } + + protected void OnSyncNetwork(NetworkInfo info) + { + bool updated = false; + + lock (_lock) + { + if (!NetworkInfo.Equals(info)) + { + NetworkInfo = info; + updated = true; + + Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"Host IP: {NetworkHelpers.ConvertUint(info.Ldn.Nodes[0].Ipv4Address)}"); + } + } + + if (updated) + { + _parent.InvokeNetworkChange(info, true); + } + + _apConnected.Set(); + } + + protected void OnConnect(LdnProxyTcpSession station) + { + lock (_lock) + { + station.NodeId = LocateEmptyNode(); + + if (_stations.Count > LdnConst.StationCountMax || station.NodeId == -1) + { + station.Disconnect(); + station.Dispose(); + + return; + } + + _stations.Add(station); + + UpdateNodes(); + } + } + + public void DisconnectStation(LdnProxyTcpSession station) + { + if (!station.IsDisposed) + { + if (station.IsConnected) + { + station.Disconnect(); + } + + station.Dispose(); + } + + lock (_lock) + { + if (_stations.Remove(station)) + { + NetworkInfo.Ldn.Nodes[station.NodeId] = new NodeInfo() + { + MacAddress = new Array6(), + UserName = new Array33(), + Reserved2 = new Array16(), + }; + + UpdateNodes(); + } + } + } + + public bool SetAdvertiseData(byte[] data) + { + if (data.Length > LdnConst.AdvertiseDataSizeMax) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "AdvertiseData exceeds size limit."); + + return false; + } + + data.CopyTo(NetworkInfo.Ldn.AdvertiseData.AsSpan()); + NetworkInfo.Ldn.AdvertiseDataSize = (ushort)data.Length; + + // NOTE: Otherwise this results in SessionKeepFailed or MasterDisconnected + lock (_lock) + { + if (NetworkInfo.Ldn.Nodes[0].IsConnected == 1) + { + UpdateNodes(true); + } + } + + return true; + } + + public void InitNetworkInfo() + { + lock (_lock) + { + NetworkInfo.Common.MacAddress = GetFakeMac(); + NetworkInfo.Common.Channel = CommonChannel; + NetworkInfo.Common.LinkLevel = CommonLinkLevel; + NetworkInfo.Common.NetworkType = CommonNetworkType; + NetworkInfo.Common.Ssid = _fakeSsid; + + NetworkInfo.Ldn.Nodes = new Array8(); + + for (int i = 0; i < LdnConst.NodeCountMax; i++) + { + NetworkInfo.Ldn.Nodes[i].NodeId = (byte)i; + NetworkInfo.Ldn.Nodes[i].IsConnected = 0; + } + } + } + + protected Array6 GetFakeMac(IPAddress address = null) + { + address ??= LocalAddr; + + byte[] ip = address.GetAddressBytes(); + + var macAddress = new Array6(); + new byte[] { 0x02, 0x00, ip[0], ip[1], ip[2], ip[3] }.CopyTo(macAddress.AsSpan()); + + return macAddress; + } + + public bool InitTcp(bool listening, IPAddress address = null, int port = DefaultPort) + { + Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"LanDiscovery InitTcp: IP: {address}, listening: {listening}"); + + if (_tcp != null) + { + _tcp.DisconnectAndStop(); + _tcp.Dispose(); + _tcp = null; + } + + ILdnTcpSocket tcpSocket; + + if (listening) + { + try + { + address ??= LocalAddr; + + tcpSocket = new LdnProxyTcpServer(_protocol, address, port); + } + catch (Exception ex) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpServer: {ex}"); + + return false; + } + + if (!tcpSocket.Start()) + { + return false; + } + } + else + { + if (address == null) + { + return false; + } + + try + { + tcpSocket = new LdnProxyTcpClient(_protocol, address, port); + } + catch (Exception ex) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpClient: {ex}"); + + return false; + } + } + + _tcp = tcpSocket; + + return true; + } + + public bool InitUdp() + { + _udp?.Stop(); + _udp2?.Stop(); + + try + { + // NOTE: Linux won't receive any broadcast packets if the socket is not bound to the broadcast address. + // Windows only works if bound to localhost or the local address. + // See this discussion: https://stackoverflow.com/questions/13666789/receiving-udp-broadcast-packets-on-linux + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + _udp2 = new LdnProxyUdpServer(_protocol, LocalBroadcastAddr, DefaultPort); + } + + _udp = new LdnProxyUdpServer(_protocol, LocalAddr, DefaultPort); + } + catch (Exception ex) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyUdpServer: {ex}"); + + return false; + } + + return true; + } + + public NetworkInfo[] Scan(ushort channel, ScanFilter filter) + { + _udp.ClearScanResults(); + + if (_protocol.SendBroadcast(_udp, LanPacketType.Scan, DefaultPort) < 0) + { + return Array.Empty(); + } + + List outNetworkInfo = new(); + + foreach (KeyValuePair item in _udp.GetScanResults()) + { + bool copy = true; + + if (filter.Flag.HasFlag(ScanFilterFlag.LocalCommunicationId)) + { + copy &= filter.NetworkId.IntentId.LocalCommunicationId == item.Value.NetworkId.IntentId.LocalCommunicationId; + } + + if (filter.Flag.HasFlag(ScanFilterFlag.SessionId)) + { + copy &= filter.NetworkId.SessionId.AsSpan().SequenceEqual(item.Value.NetworkId.SessionId.AsSpan()); + } + + if (filter.Flag.HasFlag(ScanFilterFlag.NetworkType)) + { + copy &= filter.NetworkType == (NetworkType)item.Value.Common.NetworkType; + } + + if (filter.Flag.HasFlag(ScanFilterFlag.Ssid)) + { + Span gameSsid = item.Value.Common.Ssid.Name.AsSpan()[item.Value.Common.Ssid.Length..]; + Span scanSsid = filter.Ssid.Name.AsSpan()[filter.Ssid.Length..]; + copy &= gameSsid.SequenceEqual(scanSsid); + } + + if (filter.Flag.HasFlag(ScanFilterFlag.SceneId)) + { + copy &= filter.NetworkId.IntentId.SceneId == item.Value.NetworkId.IntentId.SceneId; + } + + if (copy) + { + if (item.Value.Ldn.Nodes[0].UserName[0] != 0) + { + outNetworkInfo.Add(item.Value); + } + else + { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Scan: Got empty Username. There might be a timing issue somewhere..."); + } + } + } + + return outNetworkInfo.ToArray(); + } + + protected void ResetStations() + { + lock (_lock) + { + foreach (LdnProxyTcpSession station in _stations) + { + station.Disconnect(); + station.Dispose(); + } + + _stations.Clear(); + } + } + + private int LocateEmptyNode() + { + Array8 nodes = NetworkInfo.Ldn.Nodes; + + for (int i = 1; i < nodes.Length; i++) + { + if (nodes[i].IsConnected == 0) + { + return i; + } + } + + return -1; + } + + protected void UpdateNodes(bool forceUpdate = false) + { + int countConnected = 1; + + foreach (LdnProxyTcpSession station in _stations.Where(station => station.IsConnected)) + { + countConnected++; + + station.OverrideInfo(); + + // NOTE: This is not part of the original implementation. + NetworkInfo.Ldn.Nodes[station.NodeId] = station.NodeInfo; + } + + byte nodeCount = (byte)countConnected; + + bool networkInfoChanged = forceUpdate || NetworkInfo.Ldn.NodeCount != nodeCount; + + NetworkInfo.Ldn.NodeCount = nodeCount; + + foreach (LdnProxyTcpSession station in _stations) + { + if (station.IsConnected) + { + if (_protocol.SendPacket(station, LanPacketType.SyncNetwork, SpanHelpers.AsSpan(ref NetworkInfo).ToArray()) < 0) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to send {LanPacketType.SyncNetwork} to station {station.NodeId}"); + } + } + } + + if (networkInfoChanged) + { + _parent.InvokeNetworkChange(NetworkInfo, true); + } + } + + protected NodeInfo GetNodeInfo(NodeInfo node, UserConfig userConfig, ushort localCommunicationVersion) + { + uint ipAddress = NetworkHelpers.ConvertIpv4Address(LocalAddr); + + node.MacAddress = GetFakeMac(); + node.IsConnected = 1; + node.UserName = userConfig.UserName; + node.LocalCommunicationVersion = localCommunicationVersion; + node.Ipv4Address = ipAddress; + + return node; + } + + public bool CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig) + { + if (!InitTcp(true)) + { + return false; + } + + InitNetworkInfo(); + + NetworkInfo.Ldn.NodeCountMax = networkConfig.NodeCountMax; + NetworkInfo.Ldn.SecurityMode = (ushort)securityConfig.SecurityMode; + + NetworkInfo.Common.Channel = networkConfig.Channel == 0 ? (ushort)6 : networkConfig.Channel; + + NetworkInfo.NetworkId.SessionId = new Array16(); + _random.NextBytes(NetworkInfo.NetworkId.SessionId.AsSpan()); + NetworkInfo.NetworkId.IntentId = networkConfig.IntentId; + + NetworkInfo.Ldn.Nodes[0] = GetNodeInfo(NetworkInfo.Ldn.Nodes[0], userConfig, networkConfig.LocalCommunicationVersion); + NetworkInfo.Ldn.Nodes[0].IsConnected = 1; + NetworkInfo.Ldn.NodeCount++; + + _parent.InvokeNetworkChange(NetworkInfo, true); + + return true; + } + + public void DestroyNetwork() + { + if (_tcp != null) + { + try + { + _tcp.DisconnectAndStop(); + } + finally + { + _tcp.Dispose(); + _tcp = null; + } + } + + ResetStations(); + } + + public NetworkError Connect(NetworkInfo networkInfo, UserConfig userConfig, uint localCommunicationVersion) + { + _apConnected.Reset(); + + if (networkInfo.Ldn.NodeCount == 0) + { + return NetworkError.Unknown; + } + + IPAddress address = NetworkHelpers.ConvertUint(networkInfo.Ldn.Nodes[0].Ipv4Address); + + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Connecting to host: {address}"); + + if (!InitTcp(false, address)) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Could not initialize TCPClient"); + + return NetworkError.ConnectNotFound; + } + + if (!_tcp.Connect()) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Failed to connect."); + + return NetworkError.ConnectFailure; + } + + NodeInfo myNode = GetNodeInfo(new NodeInfo(), userConfig, (ushort)localCommunicationVersion); + if (_protocol.SendPacket(_tcp, LanPacketType.Connect, SpanHelpers.AsSpan(ref myNode).ToArray()) < 0) + { + return NetworkError.Unknown; + } + + return _apConnected.WaitOne(FailureTimeout) ? NetworkError.None : NetworkError.ConnectTimeout; + } + + public void Dispose() + { + if (_initialized) + { + DisconnectAndStop(); + ResetStations(); + _initialized = false; + } + + _protocol.Accept -= OnConnect; + _protocol.SyncNetwork -= OnSyncNetwork; + _protocol.DisconnectStation -= DisconnectStation; + } + + public void DisconnectAndStop() + { + if (_udp != null) + { + try + { + _udp.Stop(); + } + finally + { + _udp.Dispose(); + _udp = null; + } + } + + if (_udp2 != null) + { + try + { + _udp2.Stop(); + } + finally + { + _udp2.Dispose(); + _udp2 = null; + } + } + + if (_tcp != null) + { + try + { + _tcp.DisconnectAndStop(); + } + finally + { + _tcp.Dispose(); + _tcp = null; + } + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs new file mode 100644 index 000000000..f22e430bd --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs @@ -0,0 +1,314 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm +{ + internal class LanProtocol + { + private const uint LanMagic = 0x11451400; + + public const int BufferSize = 2048; + public const int TcpTxBufferSize = 0x800; + public const int TcpRxBufferSize = 0x1000; + public const int TxBufferSizeMax = 0x2000; + public const int RxBufferSizeMax = 0x2000; + + private readonly int _headerSize = Marshal.SizeOf(); + + private readonly LanDiscovery _discovery; + + public event Action Accept; + public event Action Scan; + public event Action ScanResponse; + public event Action SyncNetwork; + public event Action Connect; + public event Action DisconnectStation; + + public LanProtocol(LanDiscovery parent) + { + _discovery = parent; + } + + public void InvokeAccept(LdnProxyTcpSession session) + { + Accept?.Invoke(session); + } + + public void InvokeDisconnectStation(LdnProxyTcpSession session) + { + DisconnectStation?.Invoke(session); + } + + private void DecodeAndHandle(LanPacketHeader header, byte[] data, EndPoint endPoint = null) + { + switch (header.Type) + { + case LanPacketType.Scan: + // UDP + if (_discovery.IsHost) + { + Scan?.Invoke(endPoint, LanPacketType.ScanResponse, SpanHelpers.AsSpan(ref _discovery.NetworkInfo).ToArray()); + } + break; + case LanPacketType.ScanResponse: + // UDP + ScanResponse?.Invoke(MemoryMarshal.Cast(data)[0]); + break; + case LanPacketType.SyncNetwork: + // TCP + SyncNetwork?.Invoke(MemoryMarshal.Cast(data)[0]); + break; + case LanPacketType.Connect: + // TCP Session / Station + Connect?.Invoke(MemoryMarshal.Cast(data)[0], endPoint); + break; + default: + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decode error: Unhandled type {header.Type}"); + break; + } + } + + public void Read(scoped ref byte[] buffer, scoped ref int bufferEnd, byte[] data, int offset, int size, EndPoint endPoint = null) + { + if (endPoint != null && _discovery.LocalAddr.Equals(((IPEndPoint)endPoint).Address)) + { + return; + } + + int index = 0; + while (index < size) + { + if (bufferEnd < _headerSize) + { + int copyable2 = Math.Min(size - index, Math.Min(size, _headerSize - bufferEnd)); + + Array.Copy(data, index + offset, buffer, bufferEnd, copyable2); + + index += copyable2; + bufferEnd += copyable2; + } + + if (bufferEnd >= _headerSize) + { + LanPacketHeader header = MemoryMarshal.Cast(buffer)[0]; + if (header.Magic != LanMagic) + { + bufferEnd = 0; + + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, $"Invalid magic number in received packet. [magic: {header.Magic}] [EP: {endPoint}]"); + + return; + } + + int totalSize = _headerSize + header.Length; + if (totalSize > BufferSize) + { + bufferEnd = 0; + + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Max packet size {BufferSize} exceeded."); + + return; + } + + int copyable = Math.Min(size - index, Math.Min(size, totalSize - bufferEnd)); + + Array.Copy(data, index + offset, buffer, bufferEnd, copyable); + + index += copyable; + bufferEnd += copyable; + + if (totalSize == bufferEnd) + { + byte[] ldnData = new byte[totalSize - _headerSize]; + Array.Copy(buffer, _headerSize, ldnData, 0, ldnData.Length); + + if (header.Compressed == 1) + { + if (Decompress(ldnData, out byte[] decompressedLdnData) != 0) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error:\n {header}, {_headerSize}\n {ldnData}, {ldnData.Length}"); + + return; + } + + if (decompressedLdnData.Length != header.DecompressLength) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error: length does not match. ({decompressedLdnData.Length} != {header.DecompressLength})"); + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error data: '{string.Join("", decompressedLdnData.Select(x => (int)x).ToArray())}'"); + + return; + } + + ldnData = decompressedLdnData; + } + + DecodeAndHandle(header, ldnData, endPoint); + + bufferEnd = 0; + } + } + } + } + + public int SendBroadcast(ILdnSocket s, LanPacketType type, int port) + { + return SendPacket(s, type, Array.Empty(), new IPEndPoint(_discovery.LocalBroadcastAddr, port)); + } + + public int SendPacket(ILdnSocket s, LanPacketType type, byte[] data, EndPoint endPoint = null) + { + byte[] buf = PreparePacket(type, data); + + return s.SendPacketAsync(endPoint, buf) ? 0 : -1; + } + + public int SendPacket(LdnProxyTcpSession s, LanPacketType type, byte[] data) + { + byte[] buf = PreparePacket(type, data); + + return s.SendAsync(buf) ? 0 : -1; + } + + private LanPacketHeader PrepareHeader(LanPacketHeader header, LanPacketType type) + { + header.Magic = LanMagic; + header.Type = type; + header.Compressed = 0; + header.Length = 0; + header.DecompressLength = 0; + header.Reserved = new Array2(); + + return header; + } + + private byte[] PreparePacket(LanPacketType type, byte[] data) + { + LanPacketHeader header = PrepareHeader(new LanPacketHeader(), type); + header.Length = (ushort)data.Length; + + byte[] buf; + if (data.Length > 0) + { + if (Compress(data, out byte[] compressed) == 0) + { + header.DecompressLength = header.Length; + header.Length = (ushort)compressed.Length; + header.Compressed = 1; + + buf = new byte[compressed.Length + _headerSize]; + + SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0); + compressed.CopyTo(buf, _headerSize); + } + else + { + buf = new byte[data.Length + _headerSize]; + + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Compressing packet data failed."); + + SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0); + data.CopyTo(buf, _headerSize); + } + } + else + { + buf = new byte[_headerSize]; + SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0); + } + + return buf; + } + + private int Compress(byte[] input, out byte[] output) + { + List outputList = new(); + int i = 0; + int maxCount = 0xFF; + + while (i < input.Length) + { + byte inputByte = input[i++]; + int count = 0; + + if (inputByte == 0) + { + while (i < input.Length && input[i] == 0 && count < maxCount) + { + count += 1; + i++; + } + } + + if (inputByte == 0) + { + outputList.Add(0); + + if (outputList.Count == BufferSize) + { + output = null; + + return -1; + } + + outputList.Add((byte)count); + } + else + { + outputList.Add(inputByte); + } + } + + output = outputList.ToArray(); + + return i == input.Length ? 0 : -1; + } + + private int Decompress(byte[] input, out byte[] output) + { + List outputList = new(); + int i = 0; + + while (i < input.Length && outputList.Count < BufferSize) + { + byte inputByte = input[i++]; + + outputList.Add(inputByte); + + if (inputByte == 0) + { + if (i == input.Length) + { + output = null; + + return -1; + } + + int count = input[i++]; + + for (int j = 0; j < count; j++) + { + if (outputList.Count == BufferSize) + { + break; + } + + outputList.Add(inputByte); + } + } + } + + output = outputList.ToArray(); + + return i == input.Length ? 0 : -1; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs new file mode 100644 index 000000000..068013053 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs @@ -0,0 +1,104 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Net.NetworkInformation; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm +{ + /// + /// Client implementation for ldn_mitm + /// + internal class LdnMitmClient : INetworkClient + { + public bool NeedsRealId => false; + + public event EventHandler NetworkChange; + + private readonly LanDiscovery _lanDiscovery; + + public LdnMitmClient(HLEConfiguration config) + { + UnicastIPAddressInformation localIpInterface = NetworkHelpers.GetLocalInterface(config.MultiplayerLanInterfaceId).Item2; + + _lanDiscovery = new LanDiscovery(this, localIpInterface.Address, localIpInterface.IPv4Mask); + } + + internal void InvokeNetworkChange(NetworkInfo info, bool connected, DisconnectReason reason = DisconnectReason.None) + { + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, connected: connected, disconnectReason: reason)); + } + + public NetworkError Connect(ConnectRequest request) + { + return _lanDiscovery.Connect(request.NetworkInfo, request.UserConfig, request.LocalCommunicationVersion); + } + + public NetworkError ConnectPrivate(ConnectPrivateRequest request) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient ConnectPrivate"); + + return NetworkError.None; + } + + public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) + { + return _lanDiscovery.CreateNetwork(request.SecurityConfig, request.UserConfig, request.NetworkConfig); + } + + public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient CreateNetworkPrivate"); + + return true; + } + + public void DisconnectAndStop() + { + _lanDiscovery.DisconnectAndStop(); + } + + public void DisconnectNetwork() + { + _lanDiscovery.DestroyNetwork(); + } + + public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient Reject"); + + return ResultCode.Success; + } + + public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) + { + return _lanDiscovery.Scan(channel, scanFilter); + } + + public void SetAdvertiseData(byte[] data) + { + _lanDiscovery.SetAdvertiseData(data); + } + + public void SetGameVersion(byte[] versionString) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetGameVersion"); + } + + public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetStationAcceptPolicy"); + } + + public void Dispose() + { + _lanDiscovery.Dispose(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs new file mode 100644 index 000000000..b6e6cea9e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs @@ -0,0 +1,12 @@ +using System; +using System.Net; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal interface ILdnSocket : IDisposable + { + bool SendPacketAsync(EndPoint endpoint, byte[] buffer); + bool Start(); + bool Stop(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs new file mode 100644 index 000000000..97e3bd627 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal interface ILdnTcpSocket : ILdnSocket + { + bool Connect(); + void DisconnectAndStop(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs new file mode 100644 index 000000000..cfe9a8aae --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs @@ -0,0 +1,99 @@ +using Ryujinx.Common.Logging; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyTcpClient : NetCoreServer.TcpClient, ILdnTcpSocket + { + private readonly LanProtocol _protocol; + private byte[] _buffer; + private int _bufferEnd; + + public LdnProxyTcpClient(LanProtocol protocol, IPAddress address, int port) : base(address, port) + { + _protocol = protocol; + _buffer = new byte[LanProtocol.BufferSize]; + OptionSendBufferSize = LanProtocol.TcpTxBufferSize; + OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize; + OptionSendBufferLimit = LanProtocol.TxBufferSizeMax; + OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax; + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient connected!"); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size); + } + + public void DisconnectAndStop() + { + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + } + + public bool SendPacketAsync(EndPoint endPoint, byte[] data) + { + if (endPoint != null) + { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTcpClient is sending a packet but endpoint is not null."); + } + + if (IsConnecting && !IsConnected) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPClient needs to connect before sending packets. Waiting..."); + + while (IsConnecting && !IsConnected) + { + Thread.Yield(); + } + } + + return SendAsync(data); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient caught an error with code {error}"); + } + + protected override void Dispose(bool disposingManagedResources) + { + DisconnectAndStop(); + base.Dispose(disposingManagedResources); + } + + public override bool Connect() + { + // TODO: NetCoreServer has a Connect() method, but it currently leads to weird issues. + base.ConnectAsync(); + + while (IsConnecting) + { + Thread.Sleep(1); + } + + return IsConnected; + } + + public bool Start() + { + throw new InvalidOperationException("Start was called."); + } + + public bool Stop() + { + throw new InvalidOperationException("Stop was called."); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs new file mode 100644 index 000000000..0ca12b9f6 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs @@ -0,0 +1,54 @@ +using NetCoreServer; +using Ryujinx.Common.Logging; +using System; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyTcpServer : TcpServer, ILdnTcpSocket + { + private readonly LanProtocol _protocol; + + public LdnProxyTcpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port) + { + _protocol = protocol; + OptionReuseAddress = true; + OptionSendBufferSize = LanProtocol.TcpTxBufferSize; + OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize; + + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer created a server for this address: {address}:{port}"); + } + + protected override TcpSession CreateSession() + { + return new LdnProxyTcpSession(this, _protocol); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer caught an error with code {error}"); + } + + protected override void Dispose(bool disposingManagedResources) + { + Stop(); + base.Dispose(disposingManagedResources); + } + + public bool Connect() + { + throw new InvalidOperationException("Connect was called."); + } + + public void DisconnectAndStop() + { + Stop(); + } + + public bool SendPacketAsync(EndPoint endpoint, byte[] buffer) + { + throw new InvalidOperationException("SendPacketAsync was called."); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs new file mode 100644 index 000000000..f30c4b011 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs @@ -0,0 +1,83 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyTcpSession : NetCoreServer.TcpSession + { + private readonly LanProtocol _protocol; + + internal int NodeId; + internal NodeInfo NodeInfo; + + private byte[] _buffer; + private int _bufferEnd; + + public LdnProxyTcpSession(LdnProxyTcpServer server, LanProtocol protocol) : base(server) + { + _protocol = protocol; + _protocol.Connect += OnConnect; + _buffer = new byte[LanProtocol.BufferSize]; + OptionSendBufferSize = LanProtocol.TcpTxBufferSize; + OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize; + OptionSendBufferLimit = LanProtocol.TxBufferSizeMax; + OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax; + } + + public void OverrideInfo() + { + NodeInfo.NodeId = (byte)NodeId; + NodeInfo.IsConnected = (byte)(IsConnected ? 1 : 0); + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession connected!"); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession disconnected!"); + + _protocol.InvokeDisconnectStation(this); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, this.Socket.RemoteEndPoint); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession caught an error with code {error}"); + + Dispose(); + } + + protected override void Dispose(bool disposingManagedResources) + { + _protocol.Connect -= OnConnect; + base.Dispose(disposingManagedResources); + } + + private void OnConnect(NodeInfo info, EndPoint endPoint) + { + try + { + if (endPoint.Equals(this.Socket.RemoteEndPoint)) + { + NodeInfo = info; + _protocol.InvokeAccept(this); + } + } + catch (System.ObjectDisposedException) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession was disposed. [IP: {NodeInfo.Ipv4Address}]"); + + _protocol.InvokeDisconnectStation(this); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs new file mode 100644 index 000000000..b1519d1ff --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs @@ -0,0 +1,157 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyUdpServer : NetCoreServer.UdpServer, ILdnSocket + { + private const long ScanFrequency = 1000; + + private readonly LanProtocol _protocol; + private byte[] _buffer; + private int _bufferEnd; + + private readonly object _scanLock = new(); + + private Dictionary _scanResultsLast = new(); + private Dictionary _scanResults = new(); + private readonly AutoResetEvent _scanResponse = new(false); + private long _lastScanTime; + + public LdnProxyUdpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port) + { + _protocol = protocol; + _protocol.Scan += HandleScan; + _protocol.ScanResponse += HandleScanResponse; + _buffer = new byte[LanProtocol.BufferSize]; + OptionReuseAddress = true; + OptionReceiveBufferSize = LanProtocol.RxBufferSizeMax; + OptionSendBufferSize = LanProtocol.TxBufferSizeMax; + + Start(); + } + + protected override Socket CreateSocket() + { + return new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp) + { + EnableBroadcast = true, + }; + } + + protected override void OnStarted() + { + ReceiveAsync(); + } + + protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size) + { + _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, endpoint); + ReceiveAsync(); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyUdpServer caught an error with code {error}"); + } + + protected override void Dispose(bool disposingManagedResources) + { + _protocol.Scan -= HandleScan; + _protocol.ScanResponse -= HandleScanResponse; + + _scanResponse.Dispose(); + + base.Dispose(disposingManagedResources); + } + + public bool SendPacketAsync(EndPoint endpoint, byte[] data) + { + return SendAsync(endpoint, data); + } + + private void HandleScan(EndPoint endpoint, LanPacketType type, byte[] data) + { + _protocol.SendPacket(this, type, data, endpoint); + } + + private void HandleScanResponse(NetworkInfo info) + { + Span mac = stackalloc byte[8]; + + info.Common.MacAddress.AsSpan().CopyTo(mac); + + lock (_scanLock) + { + _scanResults[BitConverter.ToUInt64(mac)] = info; + + _scanResponse.Set(); + } + } + + public void ClearScanResults() + { + // Rate limit scans. + + long timeMs = Stopwatch.GetTimestamp() / (Stopwatch.Frequency / 1000); + long delay = ScanFrequency - (timeMs - _lastScanTime); + + if (delay > 0) + { + Thread.Sleep((int)delay); + } + + _lastScanTime = timeMs; + + lock (_scanLock) + { + var newResults = _scanResultsLast; + newResults.Clear(); + + _scanResultsLast = _scanResults; + _scanResults = newResults; + + _scanResponse.Reset(); + } + } + + public Dictionary GetScanResults() + { + // NOTE: Try to minimize waiting time for scan results. + // After we receive the first response, wait a short time for follow-ups and return. + // Responses that were too late to catch will appear in the next scan. + + // ldn_mitm does not do this, but this improves latency for games that expect it to be low (it is on console). + + if (_scanResponse.WaitOne(1000)) + { + // Wait a short while longer in case there are some other responses. + Thread.Sleep(33); + } + + lock (_scanLock) + { + var results = new Dictionary(); + + foreach (KeyValuePair last in _scanResultsLast) + { + results[last.Key] = last.Value; + } + + foreach (KeyValuePair scan in _scanResults) + { + results[scan.Key] = scan.Value; + } + + return results; + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs new file mode 100644 index 000000000..4cebe414d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 12)] + internal struct LanPacketHeader + { + public uint Magic; + public LanPacketType Type; + public byte Compressed; + public ushort Length; + public ushort DecompressLength; + public Array2 Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs new file mode 100644 index 000000000..901f00b00 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types +{ + internal enum LanPacketType : byte + { + Scan, + ScanResponse, + Connect, + SyncNetwork, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs similarity index 91% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs index 1cc09c00d..b379d2680 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class NetworkChangeEventArgs : EventArgs { diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index c190d6ed1..e39c01978 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -1,7 +1,6 @@ using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator @@ -22,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _parent.NetworkClient.NetworkChange += NetworkChanged; } - private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e) + private void NetworkChanged(object sender, NetworkChangeEventArgs e) { LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs similarity index 86% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs index 47e48d0a1..058ce62d0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { [StructLayout(LayoutKind.Sequential, Size = 0xBC)] struct ConnectPrivateRequest diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs similarity index 84% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs index 9ff46cccb..136589b2a 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { [StructLayout(LayoutKind.Sequential, Size = 0x4FC)] struct ConnectRequest diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs similarity index 88% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs index 6e890618c..ec0668884 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { /// /// Advertise data is appended separately (remaining data in the buffer). diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs similarity index 86% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs index 4efe9165a..eecea5eb0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { /// /// Advertise data is appended separately (remaining data in the buffer). diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs similarity index 80% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs index 70ebf7e38..cd576e055 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { enum NetworkError : int { diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs similarity index 71% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs index acb0b36ac..7e0c2a43f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { [StructLayout(LayoutKind.Sequential, Size = 0x4)] struct NetworkErrorMessage diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 5e3aa0eac..f3439cc8f 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index 9d2df5f03..9ed8fd8cc 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -571,6 +571,7 @@ namespace Ryujinx.Ui.Common.Configuration { LanInterfaceId = new ReactiveObject(); Mode = new ReactiveObject(); + Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode)); } } diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index f4817277d..a9d4be109 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -1121,6 +1121,14 @@ namespace Ryujinx.Ui Graphics.Gpu.GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; } + public void UpdateInternetAccess() + { + if (_gameLoaded) + { + _emulationContext.Configuration.EnableInternetAccess = ConfigurationState.Instance.System.EnableInternetAccess.Value; + } + } + public static void SaveConfig() { ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.cs b/src/Ryujinx/Ui/Windows/SettingsWindow.cs index f5186d5c1..dabef14dd 100644 --- a/src/Ryujinx/Ui/Windows/SettingsWindow.cs +++ b/src/Ryujinx/Ui/Windows/SettingsWindow.cs @@ -671,6 +671,8 @@ namespace Ryujinx.Ui.Windows } ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + _parent.UpdateInternetAccess(); MainWindow.UpdateGraphicsConfig(); ThemeHelper.ApplyTheme(); } diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.glade b/src/Ryujinx/Ui/Windows/SettingsWindow.glade index fcc8c1d19..f0dbd6b63 100644 --- a/src/Ryujinx/Ui/Windows/SettingsWindow.glade +++ b/src/Ryujinx/Ui/Windows/SettingsWindow.glade @@ -2993,6 +2993,7 @@ Disabled Disabled + ldn_mitm @@ -3064,7 +3065,7 @@ True False - The network interface used for LAN features + The network interface used for LAN/LDN features end Network Interface: @@ -3079,7 +3080,7 @@ True False - The network interface used for LAN features + The network interface used for LAN/LDN features 0 Default