From c14ce4d2a5c9b373fb454906a6dc142c028d7be2 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:32:13 +0200 Subject: [PATCH] Add ldn_mitm as a network client for LDN (#5656) * Add relevant files from private repo Hopefully I didn't miss anything. JsonHelper.cs is a debug only change I only added line 810-812 in IUserLocalCommunicationService.cs for the new Spacemeowx2Ldn case. * Add a small README.md just for fun * Add note about NetCoreServer update to 5.1.0 * Fix a few issues Fix usage of wrong broadcast address Log warning if empty userstring was received and don't add them to outNetworkInfo * Add warning about incompatibility with public LDN version * Add missing changes from old_master * Adjust ldn_mitm for Ryujinx/Ryujinx#3805 * ldn: Adapt to changes from #4582 * ldn_mitm: First cleanup iteration * ldn_mitm: Second cleanup iteration * Credit spacemeowx2 in README.md * Address first review comments by AcK Adhere to Ryujinx coding style Remove leftover log calls Change category of a few log calls Remove leftover debug notes * Replace return type with void for methods always returning true * Address first review comments by riperiperi Purely stylistic changes: - Adhere to naming style for internal fields - Improve code formatting * Throw InvalidOperationException when calling wrong ldn proxy methods * Add missing newlines in LanDiscovery.Scan() * Fix Linux not receiving broadcast packets * Remove ILdnUdpSocket It's very unlikely that we will ever need a udp client. Thus we should simplify LanDiscovery initialization and remove the parameter of InitUdp(). * ldn_mitm: Improve formatting * fixup! Fix Linux not receiving broadcast packets By opening the udp server on 'LocalBroadcastAddr' Linux refused to answer packets going to LocalAddr. So in order to fix this problem, Linux now opens two LdnProxyUdpServers. * ldn_mitm: Fix assigning incorrect NodeIds This just made connecting a lot more reliable! Thanks @riperiperi * Fix node ids when leaving/joining * Change NodeId behaviour to work like RyuLdn * Change timing for accept and network info being reported. * Wait for connection before sending anything. * Remove ConnectAsync() from ILdnTcpSocket * Only broadcast scan responses if we're hosting a network. * Fix some filters, scan network duplication. * Fix silly mistake * Don't die on duplicates, just replace. * Lock around node updates These can happen from multiple threads. * ldn_mitm: Fix namespaces for Types Improve formatting Add warning if compression failed * Add quicker scan, forgetting networks that disappear. * Always force a network sync when updating AdvertiseData * Fix TCP frame size being too large for compressed frames * Allow ldn_mitm to pass -1 id for room localcommunicationids. * ldn_mitm: Match server socket options * ldn_mitm: Use correct socket options * ldn_mitm: Remove TCP broadcast socket options * config: Rename Spacemeowx2Ldn to LdnMitm * ldn_mitm: Generate random fake SSID * ldn_mitm: Adjust logging statements/levels * ldn_mitm: Add missing Stop() call for udp2 * ldn_mitm: Adjust formatting * ldn_mitm: Add stub comments and adjust existing ones * ldn: Add LdnConst class & set tx/rx buffer sizes correctly * Move LdnConst out of UserServiceCreator Replace a few values with LdnConsts * ldn: Adjust namespaces and client names * ldn_mitm: Adjust formatting * ldn: Rename RyuLdn to LdnRyu * Replace LanProtocol.Read() refs with scoped refs * Add MIT license for ldn_mitm * Clarify that network interface is also used for LDN Although it's currently only used by ldn_mitm, it would probably be more confusing to exclude RyuLdn there. * Fix giving a station node id 0 * Update Nuget packages * Remove LdnHelper * Add update functions for EnableInternetAccess setting * ldn: Log MultiplayerMode and DisableP2P * ldn: Adjust namespaces * Apply formatting * Conform to Ryujinx code style * Remove ldn_mitm from THIRDPARTY.md It shouldn't have been there in the first place. * Improve formatting --------- Co-authored-by: riperiperi Co-authored-by: Ac_K --- Directory.Packages.props | 1 + README.md | 1 + distribution/legal/THIRDPARTY.md | 2 +- src/Ryujinx.Ava/AppHost.cs | 6 + src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 +- .../Multiplayer/MultiplayerMode.cs | 1 + .../Utilities/NetworkHelpers.cs | 5 + src/Ryujinx.HLE/HLEConfiguration.cs | 2 +- src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs | 12 + .../HOS/Services/Ldn/Types/LdnNetworkInfo.cs | 2 +- .../HOS/Services/Ldn/Types/NodeInfo.cs | 2 +- .../Services/Ldn/Types/NodeLatestUpdate.cs | 4 +- .../HOS/Services/Ldn/Types/SecurityConfig.cs | 2 +- .../HOS/Services/Ldn/Types/Ssid.cs | 2 +- .../HOS/Services/Ldn/Types/UserConfig.cs | 2 +- .../Ldn/UserServiceCreator/AccessPoint.cs | 5 +- .../{RyuLdn => }/INetworkClient.cs | 7 +- .../IUserLocalCommunicationService.cs | 28 +- ...abledLdnClient.cs => LdnDisabledClient.cs} | 9 +- .../LdnMitm/LanDiscovery.cs | 611 ++++++++++++++++++ .../UserServiceCreator/LdnMitm/LanProtocol.cs | 314 +++++++++ .../LdnMitm/LdnMitmClient.cs | 104 +++ .../LdnMitm/Proxy/ILdnSocket.cs | 12 + .../LdnMitm/Proxy/ILdnTcpSocket.cs | 8 + .../LdnMitm/Proxy/LdnProxyTcpClient.cs | 99 +++ .../LdnMitm/Proxy/LdnProxyTcpServer.cs | 54 ++ .../LdnMitm/Proxy/LdnProxyTcpSession.cs | 83 +++ .../LdnMitm/Proxy/LdnProxyUdpServer.cs | 157 +++++ .../LdnMitm/Types/LanPacketHeader.cs | 16 + .../LdnMitm/Types/LanPacketType.cs | 10 + .../{RyuLdn => }/NetworkChangeEventArgs.cs | 2 +- .../Ldn/UserServiceCreator/Station.cs | 5 +- .../Types/ConnectPrivateRequest.cs | 2 +- .../{Network => }/Types/ConnectRequest.cs | 2 +- .../Types/CreateAccessPointPrivateRequest.cs | 2 +- .../Types/CreateAccessPointRequest.cs | 2 +- .../{RyuLdn => }/Types/NetworkError.cs | 2 +- .../{RyuLdn => }/Types/NetworkErrorMessage.cs | 2 +- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 1 + .../Configuration/ConfigurationState.cs | 1 + src/Ryujinx/Ui/MainWindow.cs | 8 + src/Ryujinx/Ui/Windows/SettingsWindow.cs | 2 + src/Ryujinx/Ui/Windows/SettingsWindow.glade | 5 +- 43 files changed, 1556 insertions(+), 43 deletions(-) create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/INetworkClient.cs (81%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn/DisabledLdnClient.cs => LdnDisabledClient.cs} (87%) create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/NetworkChangeEventArgs.cs (91%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/ConnectPrivateRequest.cs (86%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{Network => }/Types/ConnectRequest.cs (84%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/CreateAccessPointPrivateRequest.cs (88%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{Network => }/Types/CreateAccessPointRequest.cs (86%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/NetworkError.cs (80%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/NetworkErrorMessage.cs (71%) 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