0
0
Fork 0
mirror of https://github.com/ryujinx-mirror/ryujinx.git synced 2024-10-18 12:21:40 +00:00

RyuLDN implementation from Berry's fork. Logo and unrelated changes have been removed.

This commit is contained in:
Vudjun 2024-10-13 16:35:45 +01:00
parent c4ee9c7555
commit 0974f75064
80 changed files with 4104 additions and 154 deletions

View file

@ -31,6 +31,7 @@
<PackageVersion Include="OpenTK.Graphics" Version="4.8.2" />
<PackageVersion Include="OpenTK.Audio.OpenAL" Version="4.8.2" />
<PackageVersion Include="OpenTK.Windowing.GraphicsLibraryFramework" Version="4.8.2" />
<PackageVersion Include="Open.NAT.Core" Version="2.1.0.5" />
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
@ -49,4 +50,4 @@
<PackageVersion Include="System.Management" Version="8.0.0" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
</ItemGroup>
</Project>
</Project>

View file

@ -3,6 +3,7 @@ namespace Ryujinx.Common.Configuration.Multiplayer
public enum MultiplayerMode
{
Disabled,
LdnRyu,
LdnMitm,
}
}

View file

@ -803,18 +803,6 @@ namespace Ryujinx.Common.Memory
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array256<T> : IArray<T> where T : unmanaged
{
T _e0;
Array128<T> _other;
Array127<T> _other2;
public readonly int Length => 256;
public ref T this[int index] => ref AsSpan()[index];
[Pure]
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array140<T> : IArray<T> where T : unmanaged
{
T _e0;
@ -828,6 +816,19 @@ namespace Ryujinx.Common.Memory
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array256<T> : IArray<T> where T : unmanaged
{
T _e0;
Array128<T> _other;
Array127<T> _other2;
public readonly int Length => 256;
public ref T this[int index] => ref AsSpan()[index];
[Pure]
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array384<T> : IArray<T> where T : unmanaged
{
T _e0;

View file

@ -1,6 +1,7 @@
using System.Buffers.Binary;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
namespace Ryujinx.Common.Utilities
{
@ -65,6 +66,11 @@ namespace Ryujinx.Common.Utilities
return (targetProperties, targetAddressInfo);
}
public static bool SupportsDynamicDns()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
}
public static uint ConvertIpv4Address(IPAddress ipAddress)
{
return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes());

View file

@ -9,6 +9,7 @@
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
<!-- As we already provide GTK3 on Windows via GtkSharp.Dependencies this is redundant. -->
<SkipGtkInstall>true</SkipGtkInstall>
<ApplicationIcon>Ryujinx.ico</ApplicationIcon>
<TieredPGO>true</TieredPGO>
</PropertyGroup>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View file

@ -115,6 +115,7 @@ namespace Ryujinx.UI
[GUI] CheckMenuItem _appToggle;
[GUI] CheckMenuItem _timePlayedToggle;
[GUI] CheckMenuItem _versionToggle;
[GUI] CheckMenuItem _ldnInfoToggle;
[GUI] CheckMenuItem _lastPlayedToggle;
[GUI] CheckMenuItem _fileExtToggle;
[GUI] CheckMenuItem _pathToggle;
@ -218,6 +219,8 @@ namespace Ryujinx.UI
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerMode;
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateMultiplayerLanInterfaceId;
ConfigurationState.Instance.Multiplayer.LdnPassphrase.Event += UpdateMultiplayerPassphrase;
ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateMultiplayerDisableP2p;
if (ConfigurationState.Instance.UI.StartFullscreen)
{
@ -267,6 +270,10 @@ namespace Ryujinx.UI
{
_versionToggle.Active = true;
}
if (ConfigurationState.Instance.UI.GuiColumns.LdnInfoColumn)
{
_ldnInfoToggle.Active = true;
}
if (ConfigurationState.Instance.UI.GuiColumns.TimePlayedColumn)
{
_timePlayedToggle.Active = true;
@ -310,11 +317,12 @@ namespace Ryujinx.UI
typeof(string),
typeof(string),
typeof(string),
typeof(string),
typeof(BlitStruct<ApplicationControlProperty>));
_tableStore.SetSortFunc(5, SortHelper.TimePlayedSort);
_tableStore.SetSortFunc(6, SortHelper.LastPlayedSort);
_tableStore.SetSortFunc(8, SortHelper.FileSizeSort);
_tableStore.SetSortFunc(6, SortHelper.TimePlayedSort);
_tableStore.SetSortFunc(7, SortHelper.LastPlayedSort);
_tableStore.SetSortFunc(9, SortHelper.FileSizeSort);
int columnId = ConfigurationState.Instance.UI.ColumnSort.SortColumnId;
bool ascending = ConfigurationState.Instance.UI.ColumnSort.SortAscending;
@ -337,6 +345,14 @@ namespace Ryujinx.UI
}
};
ConfigurationState.Instance.Multiplayer.Mode.Event += (sender, args) =>
{
if (args.OldValue != args.NewValue)
{
UpdateColumns();
}
};
Task.Run(RefreshFirmwareLabel);
InputManager = new InputManager(new GTK3KeyboardDriver(this), new SDL2GamepadDriver());
@ -358,6 +374,22 @@ namespace Ryujinx.UI
}
}
private void UpdateMultiplayerDisableP2p(object sender, ReactiveEventArgs<bool> args)
{
if (_emulationContext != null)
{
_emulationContext.Configuration.MultiplayerDisableP2p = args.NewValue;
}
}
private void UpdateMultiplayerPassphrase(object sender, ReactiveEventArgs<string> args)
{
if (_emulationContext != null)
{
_emulationContext.Configuration.MultiplayerLdnPassphrase = args.NewValue;
}
}
private void UpdateIgnoreMissingServicesState(object sender, ReactiveEventArgs<bool> args)
{
if (_emulationContext != null)
@ -429,6 +461,11 @@ namespace Ryujinx.UI
{
_gameTable.AppendColumn("Version", new CellRendererText(), "text", 4);
}
if (ConfigurationState.Instance.Multiplayer.Mode.Value != MultiplayerMode.Disabled
&& ConfigurationState.Instance.UI.GuiColumns.LdnInfoColumn)
{
_gameTable.AppendColumn("LDN Info", new CellRendererText(), "text", 5);
}
if (ConfigurationState.Instance.UI.GuiColumns.TimePlayedColumn)
{
_gameTable.AppendColumn("Time Played", new CellRendererText(), "text", 5);
@ -470,26 +507,30 @@ namespace Ryujinx.UI
column.SortColumnId = 4;
column.Clicked += Column_Clicked;
break;
case "Time Played":
case "LDN Info":
column.SortColumnId = 5;
column.Clicked += Column_Clicked;
break;
case "Last Played":
case "Time Played":
column.SortColumnId = 6;
column.Clicked += Column_Clicked;
break;
case "File Ext":
case "Last Played":
column.SortColumnId = 7;
column.Clicked += Column_Clicked;
break;
case "File Size":
case "File Ext":
column.SortColumnId = 8;
column.Clicked += Column_Clicked;
break;
case "Path":
case "File Size":
column.SortColumnId = 9;
column.Clicked += Column_Clicked;
break;
case "Path":
column.SortColumnId = 10;
column.Clicked += Column_Clicked;
break;
}
}
}
@ -677,7 +718,9 @@ namespace Ryujinx.UI
ConfigurationState.Instance.System.AudioVolume,
ConfigurationState.Instance.System.UseHypervisor,
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value,
ConfigurationState.Instance.Multiplayer.Mode);
ConfigurationState.Instance.Multiplayer.Mode,
ConfigurationState.Instance.Multiplayer.DisableP2p,
ConfigurationState.Instance.Multiplayer.LdnPassphrase);
_emulationContext = new HLE.Switch(configuration);
}
@ -738,10 +781,10 @@ namespace Ryujinx.UI
_tableStore.Clear();
Thread applicationLibraryThread = new(() =>
Thread applicationLibraryThread = new(async () =>
{
ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language;
ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs);
await ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs);
_updatingGameTable = false;
})
@ -1177,6 +1220,7 @@ namespace Ryujinx.UI
$"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}",
args.AppData.Developer,
args.AppData.Version,
(args.AppData.GameCount == 0 && args.AppData.PlayerCount == 0) ? "N/A" : $"Hosted Games: {args.AppData.GameCount}\nOnline Players: {args.AppData.PlayerCount}",
args.AppData.TimePlayedString,
args.AppData.LastPlayedString,
args.AppData.FileExtension,
@ -1269,12 +1313,12 @@ namespace Ryujinx.UI
Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber),
Developer = (string)_tableStore.GetValue(treeIter, 3),
Version = (string)_tableStore.GetValue(treeIter, 4),
TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)),
LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)),
FileExtension = (string)_tableStore.GetValue(treeIter, 7),
FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)),
Path = (string)_tableStore.GetValue(treeIter, 9),
ControlHolder = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10),
TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 6)),
LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 7)),
FileExtension = (string)_tableStore.GetValue(treeIter, 8),
FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 9)),
Path = (string)_tableStore.GetValue(treeIter, 10),
ControlHolder = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 11),
};
RunApplication(application);
@ -1897,6 +1941,14 @@ namespace Ryujinx.UI
UpdateColumns();
}
private void LdnInfo_Toggled(object sender, EventArgs args)
{
ConfigurationState.Instance.UI.GuiColumns.LdnInfoColumn.Value = _ldnInfoToggle.Active;
SaveConfig();
UpdateColumns();
}
private void TimePlayed_Toggled(object sender, EventArgs args)
{
ConfigurationState.Instance.UI.GuiColumns.TimePlayedColumn.Value = _timePlayedToggle.Active;

View file

@ -195,15 +195,6 @@
<property name="use-underline">True</property>
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="_developerToggle">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Enable or Disable Developer Column in the game list</property>
<property name="label" translatable="yes">Enable Developer Column</property>
<property name="use-underline">True</property>
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="_versionToggle">
<property name="visible">True</property>
@ -211,6 +202,27 @@
<property name="tooltip-text" translatable="yes">Enable or Disable Version Column in the game list</property>
<property name="label" translatable="yes">Enable Version Column</property>
<property name="use-underline">True</property>
<signal name="toggled" handler="Version_Toggled" swapped="no"/>
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="_ldnInfoToggle">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Enable or Disable LDN Info Column in the game list</property>
<property name="label" translatable="yes">Enable LDN Info Column</property>
<property name="use_underline">True</property>
<signal name="toggled" handler="LdnInfo_Toggled" swapped="no"/>
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="_developerToggle">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Enable or Disable Developer Column in the game list</property>
<property name="label" translatable="yes">Enable Developer Column</property>
<property name="use-underline">True</property>
<signal name="toggled" handler="Developer_Toggled" swapped="no"/>
</object>
</child>
<child>

View file

@ -90,6 +90,11 @@ namespace Ryujinx.UI.Windows
[GUI] Adjustment _systemTimeMinuteSpinAdjustment;
[GUI] ComboBoxText _multiLanSelect;
[GUI] ComboBoxText _multiModeSelect;
[GUI] CheckButton _multiP2pDisable;
[GUI] Entry _multiLdnPassphraseEntry;
[GUI] Button _multiLdnPassphraseRandom;
[GUI] Button _multiLdnPassphraseClear;
[GUI] Label _multiInvalidPassphraseLabel;
[GUI] CheckButton _custThemeToggle;
[GUI] Entry _custThemePath;
[GUI] ToggleButton _browseThemePath;
@ -156,6 +161,9 @@ namespace Ryujinx.UI.Windows
GtkDialog.CreateInfoDialog("Warning - Backend Threading", "Ryujinx must be restarted after changing this option for it to apply fully. Depending on your platform, you may need to manually disable your driver's own multithreading when using Ryujinx's.");
}
};
_multiLdnPassphraseEntry.Changed += (sender, args) => ValidateLdnPassphrase();
_multiLdnPassphraseRandom.Clicked += ClickRandomPassphrase;
_multiLdnPassphraseClear.Clicked += ClearRandomPassphrase;
// Setup Currents.
if (ConfigurationState.Instance.Logger.EnableTrace)
@ -375,6 +383,10 @@ namespace Ryujinx.UI.Windows
_fsLogSpinAdjustment.Value = ConfigurationState.Instance.System.FsGlobalAccessLogMode;
_systemTimeOffset = ConfigurationState.Instance.System.SystemTimeOffset;
_multiP2pDisable.Active = ConfigurationState.Instance.Multiplayer.DisableP2p;
_multiLdnPassphraseEntry.Buffer.Text = ConfigurationState.Instance.Multiplayer.LdnPassphrase;
ValidateLdnPassphrase();
_gameDirsBox.AppendColumn("", new CellRendererText(), "text", 0);
_gameDirsBoxStore = new ListStore(typeof(string));
_gameDirsBox.Model = _gameDirsBoxStore;
@ -520,6 +532,34 @@ namespace Ryujinx.UI.Windows
}
}
private void ValidateLdnPassphrase()
{
string passphrase = _multiLdnPassphraseEntry.Buffer.Text;
Regex match = new Regex("Ryujinx-[0-9a-f]{8}");
bool valid = passphrase == "" || (passphrase.Length == 16 && match.IsMatch(passphrase));
_multiInvalidPassphraseLabel.Visible = !valid;
}
private void ClearRandomPassphrase(object sender, EventArgs e)
{
_multiLdnPassphraseEntry.Buffer.Text = "";
}
private void ClickRandomPassphrase(object sender, EventArgs e)
{
Random random = new Random();
byte[] code = new byte[4];
random.NextBytes(code);
uint codeUint = BitConverter.ToUInt32(code);
_multiLdnPassphraseEntry.Buffer.Text = $"Ryujinx-{codeUint:x8}";
}
private void UpdateSystemTimeSpinners()
{
//Bind system time events
@ -664,6 +704,8 @@ namespace Ryujinx.UI.Windows
ConfigurationState.Instance.Multiplayer.Mode.Value = Enum.Parse<MultiplayerMode>(_multiModeSelect.ActiveId);
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId;
ConfigurationState.Instance.Multiplayer.DisableP2p.Value = _multiP2pDisable.Active;
ConfigurationState.Instance.Multiplayer.LdnPassphrase.Value = _multiLdnPassphraseEntry.Text;
if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter))
{

View file

@ -2966,6 +2966,33 @@
<property name="margin-left">10</property>
<property name="margin-right">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="MultiTextBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="wrap">True</property>
<property name="halign">start</property>
<property name="margin-bottom">5</property>
<property name="label" translatable="yes">Before going online, make sure to configure your user profile under Options > Manage User Profiles, as other players will see your profile name and image.</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="ModeBox">
<property name="visible">True</property>
@ -2990,9 +3017,10 @@
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Change Multiplayer Mode</property>
<property name="active-id">Disabled</property>
<property name="active-id">LdnRyu</property>
<items>
<item id="Disabled" translatable="yes">Disabled</item>
<item id="LdnRyu" translatable="yes">Ryujinx Ldn</item>
<item id="LdnMitm" translatable="yes">ldn_mitm</item>
</items>
</object>
@ -3002,6 +3030,112 @@
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="_multiP2pDisable">
<property name="label" translatable="yes">Disable P2P Network Hosting (may increase latency)</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Disable P2P networking, instead proxying through the master server instead of directly to the host.</property>
<property name="halign">start</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="LdnPassphraseBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">You will only be able to see hosted games with the same passphrase as you.</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Network Passphrase:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="_multiLdnPassphraseEntry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="tooltip-text" translatable="yes">Enter a passphrase in the format Ryujinx-&lt;8 hex chars&gt;. You will only be able to see hosted games with the same passphrase as you.</property>
<property name="max-length">16</property>
<property name="placeholder-text" translatable="yes">(public)</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_multiLdnPassphraseRandom">
<property name="label" translatable="yes">Generate Random</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Generates a new passphrase, which can be shared with other players.</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_multiLdnPassphraseClear">
<property name="label" translatable="yes">Clear</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Clears the current passphrase, returning to the public network.</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="_multiInvalidPassphraseLabel">
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="margin-left">5</property>
<property name="label" translatable="yes">Invalid Passphrase! Must be in the format "Ryujinx-&lt;8 hex chars&gt;".</property>
<attributes>
<attribute name="style" value="italic"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>

View file

@ -164,6 +164,16 @@ namespace Ryujinx.HLE
/// </summary>
public MultiplayerMode MultiplayerMode { internal get; set; }
/// <summary>
/// Disable P2P mode
/// </summary>
public bool MultiplayerDisableP2p { internal get; set; }
/// <summary>
/// Multiplayer Passphrase
/// </summary>
public string MultiplayerLdnPassphrase { internal get; set; }
/// <summary>
/// An action called when HLE force a refresh of output after docked mode changed.
/// </summary>
@ -194,7 +204,9 @@ namespace Ryujinx.HLE
float audioVolume,
bool useHypervisor,
string multiplayerLanInterfaceId,
MultiplayerMode multiplayerMode)
MultiplayerMode multiplayerMode,
bool multiplayerDisableP2p,
string multiplayerLdnPassphrase)
{
VirtualFileSystem = virtualFileSystem;
LibHacHorizonManager = libHacHorizonManager;
@ -222,6 +234,8 @@ namespace Ryujinx.HLE
UseHypervisor = useHypervisor;
MultiplayerLanInterfaceId = multiplayerLanInterfaceId;
MultiplayerMode = multiplayerMode;
MultiplayerDisableP2p = multiplayerDisableP2p;
MultiplayerLdnPassphrase = multiplayerLdnPassphrase;
}
}
}

View file

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x20)]
[StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 8)]
struct NetworkConfig
{
public IntentId IntentId;

View file

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x60)]
[StructLayout(LayoutKind.Sequential, Size = 0x60, Pack = 8)]
struct ScanFilter
{
public NetworkId NetworkId;

View file

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x44)]
[StructLayout(LayoutKind.Sequential, Size = 0x44, Pack = 2)]
struct SecurityConfig
{
public SecurityMode SecurityMode;

View file

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x20)]
[StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 1)]
struct SecurityParameter
{
public Array16<byte> Data;

View file

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x30)]
[StructLayout(LayoutKind.Sequential, Size = 0x30, Pack = 1)]
struct UserConfig
{
public Array33<byte> UserName;

View file

@ -13,8 +13,11 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public NetworkInfo NetworkInfo;
public Array8<NodeLatestUpdate> LatestUpdates = new();
public bool Connected { get; private set; }
public ProxyConfig Config => _parent.NetworkClient.Config;
public AccessPoint(IUserLocalCommunicationService parent)
{
_parent = parent;

View file

@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
interface INetworkClient : IDisposable
{
ProxyConfig Config { get; }
bool NeedsRealId { get; }
event EventHandler<NetworkChangeEventArgs> NetworkChange;

View file

@ -9,10 +9,14 @@ 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.LdnMitm;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.Horizon.Common;
using Ryujinx.Memory;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
@ -21,6 +25,9 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
class IUserLocalCommunicationService : IpcService, IDisposable
{
public static string LanPlayHost = "ldn.ryujinx.org";
public static short LanPlayPort = 30456;
public INetworkClient NetworkClient { get; private set; }
private const int NifmRequestID = 90;
@ -175,19 +182,37 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected)
{
(_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId);
if (unicastAddress == null)
ProxyConfig config = _state switch
{
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress));
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask));
NetworkState.AccessPointCreated => _accessPoint.Config,
NetworkState.StationConnected => _station.Config,
_ => default
};
if (config.ProxyIp == 0)
{
(_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId);
if (unicastAddress == null)
{
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress));
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask));
}
else
{
Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address));
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
}
}
else
{
Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP.");
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address));
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
context.ResponseData.Write(config.ProxyIp);
context.ResponseData.Write(config.ProxySubnetMask);
}
}
else
@ -1066,6 +1091,21 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
switch (mode)
{
case MultiplayerMode.LdnRyu:
try
{
if (!IPAddress.TryParse(LanPlayHost, out IPAddress ipAddress))
{
ipAddress = Dns.GetHostEntry(LanPlayHost).AddressList[0];
}
NetworkClient = new LdnMasterProxyClient(ipAddress.ToString(), LanPlayPort, context.Device.Configuration);
}
catch (Exception)
{
Logger.Error?.Print(LogClass.ServiceLdn, "Could not locate LdnRyu server. Defaulting to stubbed wireless.");
NetworkClient = new LdnDisabledClient();
}
break;
case MultiplayerMode.LdnMitm:
NetworkClient = new LdnMitmClient(context.Device.Configuration);
break;
@ -1103,7 +1143,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_accessPoint?.Dispose();
_accessPoint = null;
NetworkClient?.Dispose();
NetworkClient?.DisconnectAndStop();
NetworkClient = null;
}
}

View file

@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
class LdnDisabledClient : INetworkClient
{
public ProxyConfig Config { get; }
public bool NeedsRealId => true;
public event EventHandler<NetworkChangeEventArgs> NetworkChange;

View file

@ -12,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
/// </summary>
internal class LdnMitmClient : INetworkClient
{
public ProxyConfig Config { get; }
public bool NeedsRealId => false;
public event EventHandler<NetworkChangeEventArgs> NetworkChange;

View file

@ -0,0 +1,7 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
{
interface IProxyClient
{
bool SendAsync(byte[] buffer);
}
}

View file

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

View file

@ -0,0 +1,83 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
{
class NetworkTimeout : IDisposable
{
private int _idleTimeout;
private Action _timeoutCallback;
private CancellationTokenSource _cancel;
private object _lock = new object();
public NetworkTimeout(int idleTimeout, Action timeoutCallback)
{
_idleTimeout = idleTimeout;
_timeoutCallback = timeoutCallback;
}
private async Task TimeoutTask()
{
CancellationTokenSource cts;
lock (_lock)
{
cts = _cancel;
}
if (cts == null)
{
return;
}
try
{
await Task.Delay(_idleTimeout, cts.Token);
}
catch (TaskCanceledException)
{
return; // Timeout cancelled.
}
lock (_lock)
{
// Run the timeout callback. If the cancel token source has been replaced, we have _just_ been cancelled.
if (cts == _cancel)
{
_timeoutCallback();
}
}
}
public bool RefreshTimeout()
{
lock (_lock)
{
_cancel?.Cancel();
_cancel = new CancellationTokenSource();
Task.Run(TimeoutTask);
}
return true;
}
public void DisableTimeout()
{
lock (_lock)
{
_cancel?.Cancel();
_cancel = new CancellationTokenSource();
}
}
public void Dispose()
{
DisableTimeout();
}
}
}

View file

@ -0,0 +1,53 @@
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
public class EphemeralPortPool
{
private const ushort EphemeralBase = 49152;
private List<ushort> _ephemeralPorts = new List<ushort>();
private object _lock = new object();
public ushort Get()
{
ushort port = EphemeralBase;
lock (_lock)
{
// Starting at the ephemeral port base, return an ephemeral port that is not in use.
// Returns 0 if the range is exhausted.
for (int i = 0; i < _ephemeralPorts.Count; i++)
{
ushort existingPort = _ephemeralPorts[i];
if (existingPort > port)
{
// The port was free - take it.
_ephemeralPorts.Insert(i, port);
return port;
}
port++;
}
if (port != 0)
{
_ephemeralPorts.Add(port);
}
return port;
}
}
public void Return(ushort port)
{
lock (_lock)
{
_ephemeralPorts.Remove(port);
}
}
}
}

View file

@ -0,0 +1,259 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
class LdnProxy : IDisposable
{
public EndPoint LocalEndpoint { get; }
public IPAddress LocalAddress { get; }
private List<LdnProxySocket> _sockets = new List<LdnProxySocket>();
private Dictionary<ProtocolType, EphemeralPortPool> _ephemeralPorts = new Dictionary<ProtocolType, EphemeralPortPool>();
private IProxyClient _parent;
private RyuLdnProtocol _protocol;
private uint _subnetMask;
private uint _localIp;
private uint _broadcast;
public LdnProxy(ProxyConfig config, IProxyClient client, RyuLdnProtocol protocol)
{
_parent = client;
_protocol = protocol;
_ephemeralPorts[ProtocolType.Udp] = new EphemeralPortPool();
_ephemeralPorts[ProtocolType.Tcp] = new EphemeralPortPool();
byte[] address = BitConverter.GetBytes(config.ProxyIp);
Array.Reverse(address);
LocalAddress = new IPAddress(address);
_subnetMask = config.ProxySubnetMask;
_localIp = config.ProxyIp;
_broadcast = _localIp | (~_subnetMask);
RegisterHandlers(protocol);
}
public bool Supported(AddressFamily domain, SocketType type, ProtocolType protocol)
{
if (protocol == ProtocolType.Tcp)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Tcp proxy networking is untested. Please report this game so that it can be tested.");
}
return domain == AddressFamily.InterNetwork && (protocol == ProtocolType.Tcp || protocol == ProtocolType.Udp);
}
private void RegisterHandlers(RyuLdnProtocol protocol)
{
protocol.ProxyConnect += HandleConnectionRequest;
protocol.ProxyConnectReply += HandleConnectionResponse;
protocol.ProxyData += HandleData;
protocol.ProxyDisconnect += HandleDisconnect;
_protocol = protocol;
}
public void UnregisterHandlers(RyuLdnProtocol protocol)
{
protocol.ProxyConnect -= HandleConnectionRequest;
protocol.ProxyConnectReply -= HandleConnectionResponse;
protocol.ProxyData -= HandleData;
protocol.ProxyDisconnect -= HandleDisconnect;
}
public ushort GetEphemeralPort(ProtocolType type)
{
return _ephemeralPorts[type].Get();
}
public void ReturnEphemeralPort(ProtocolType type, ushort port)
{
_ephemeralPorts[type].Return(port);
}
public void RegisterSocket(LdnProxySocket socket)
{
lock (_sockets)
{
_sockets.Add(socket);
}
}
public void UnregisterSocket(LdnProxySocket socket)
{
lock (_sockets)
{
_sockets.Remove(socket);
}
}
private void ForRoutedSockets(ProxyInfo info, Action<LdnProxySocket> action)
{
lock (_sockets)
{
foreach (LdnProxySocket socket in _sockets)
{
// Must match protocol and destination port.
if (socket.ProtocolType != info.Protocol || !(socket.LocalEndPoint is IPEndPoint endpoint) || endpoint.Port != info.DestPort)
{
continue;
}
// We can assume packets routed to us have been sent to our destination.
// They will either be sent to us, or broadcast packets.
action(socket);
}
}
}
public void HandleConnectionRequest(LdnHeader header, ProxyConnectRequest request)
{
ForRoutedSockets(request.Info, (socket) =>
{
socket.HandleConnectRequest(request);
});
}
public void HandleConnectionResponse(LdnHeader header, ProxyConnectResponse response)
{
ForRoutedSockets(response.Info, (socket) =>
{
socket.HandleConnectResponse(response);
});
}
private string IPToString(uint ip)
{
return $"{(byte)(ip >> 24)}.{(byte)(ip >> 16)}.{(byte)(ip >> 8)}.{(byte)ip}";
}
public void HandleData(LdnHeader header, ProxyDataHeader proxyHeader, byte[] data)
{
ProxyDataPacket packet = new ProxyDataPacket() { Header = proxyHeader, Data = data };
ForRoutedSockets(proxyHeader.Info, (socket) =>
{
socket.IncomingData(packet);
});
}
public void HandleDisconnect(LdnHeader header, ProxyDisconnectMessage disconnect)
{
ForRoutedSockets(disconnect.Info, (socket) =>
{
socket.HandleDisconnect(disconnect);
});
}
private uint GetIpV4(IPEndPoint endpoint)
{
if (endpoint.AddressFamily != AddressFamily.InterNetwork)
{
throw new NotSupportedException();
}
byte[] address = endpoint.Address.GetAddressBytes();
Array.Reverse(address);
return BitConverter.ToUInt32(address);
}
private ProxyInfo MakeInfo(IPEndPoint localEp, IPEndPoint remoteEP, ProtocolType type)
{
return new ProxyInfo
{
SourceIpV4 = GetIpV4(localEp),
SourcePort = (ushort)localEp.Port,
DestIpV4 = GetIpV4(remoteEP),
DestPort = (ushort)remoteEP.Port,
Protocol = type
};
}
public void RequestConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
{
// We must ask the other side to initialize a connection, so they can accept a socket for us.
ProxyConnectRequest request = new ProxyConnectRequest
{
Info = MakeInfo(localEp, remoteEp, type)
};
_parent.SendAsync(_protocol.Encode(PacketId.ProxyConnect, request));
}
public void SignalConnected(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
{
// We must tell the other side that we have accepted their request for connection.
ProxyConnectResponse request = new ProxyConnectResponse
{
Info = MakeInfo(localEp, remoteEp, type)
};
_parent.SendAsync(_protocol.Encode(PacketId.ProxyConnectReply, request));
}
public void EndConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
{
// We must tell the other side that our connection is dropped.
ProxyDisconnectMessage request = new ProxyDisconnectMessage
{
Info = MakeInfo(localEp, remoteEp, type),
DisconnectReason = 0 // TODO
};
_parent.SendAsync(_protocol.Encode(PacketId.ProxyDisconnect, request));
}
public int SendTo(ReadOnlySpan<byte> buffer, SocketFlags flags, IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
{
// We send exactly as much as the user wants us to, currently instantly.
// TODO: handle over "virtual mtu" (we have a max packet size to worry about anyways). fragment if tcp? throw if udp?
ProxyDataHeader request = new ProxyDataHeader
{
Info = MakeInfo(localEp, remoteEp, type),
DataLength = (uint)buffer.Length
};
_parent.SendAsync(_protocol.Encode(PacketId.ProxyData, request, buffer.ToArray()));
return buffer.Length;
}
public bool IsBroadcast(uint ip)
{
return ip == _broadcast;
}
public bool IsMyself(uint ip)
{
return ip == _localIp;
}
public void Dispose()
{
UnregisterHandlers(_protocol);
lock (_sockets)
{
foreach (LdnProxySocket socket in _sockets)
{
socket.ProxyDestroyed();
}
}
}
}
}

View file

@ -0,0 +1,795 @@
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
/// <summary>
/// This socket is forwarded through a TCP stream that goes through the Ldn server.
/// The Ldn server will then route the packets we send (or need to receive) within the virtual adhoc network.
/// </summary>
class LdnProxySocket : ISocketImpl
{
private LdnProxy _proxy;
private bool _isListening;
private List<LdnProxySocket> _listenSockets = new List<LdnProxySocket>();
private Queue<ProxyConnectRequest> _connectRequests = new Queue<ProxyConnectRequest>();
private AutoResetEvent _acceptEvent = new AutoResetEvent(false);
private int _acceptTimeout = -1;
private Queue<int> _errors = new Queue<int>();
private AutoResetEvent _connectEvent = new AutoResetEvent(false);
private ProxyConnectResponse _connectResponse;
private int _receiveTimeout = -1;
private AutoResetEvent _receiveEvent = new AutoResetEvent(false);
private Queue<ProxyDataPacket> _receiveQueue = new Queue<ProxyDataPacket>();
private int _sendTimeout = -1; // Sends are techically instant right now, so not _really_ used.
private bool _connecting;
private bool _broadcast;
private bool _readShutdown;
private bool _writeShutdown;
private bool _closed;
private Dictionary<SocketOptionName, int> _socketOptions = new Dictionary<SocketOptionName, int>()
{
{ SocketOptionName.Broadcast, 0 }, //TODO: honor this value
{ SocketOptionName.DontLinger, 0 },
{ SocketOptionName.Debug, 0 },
{ SocketOptionName.Error, 0 },
{ SocketOptionName.KeepAlive, 0 },
{ SocketOptionName.OutOfBandInline, 0 },
{ SocketOptionName.ReceiveBuffer, 131072 },
{ SocketOptionName.ReceiveTimeout, -1 },
{ SocketOptionName.SendBuffer, 131072 },
{ SocketOptionName.SendTimeout, -1 },
{ SocketOptionName.Type, 0 },
{ SocketOptionName.ReuseAddress, 0 } //TODO: honor this value
};
public EndPoint RemoteEndPoint { get; private set; }
public EndPoint LocalEndPoint { get; private set; }
public bool Connected { get; private set; }
public bool IsBound { get; private set; }
public AddressFamily AddressFamily { get; }
public SocketType SocketType { get; }
public ProtocolType ProtocolType { get; }
public bool Blocking { get; set; }
public int Available
{
get
{
int result = 0;
lock (_receiveQueue)
{
foreach (ProxyDataPacket data in _receiveQueue)
{
result += data.Data.Length;
}
}
return result;
}
}
public bool Readable
{
get
{
if (_isListening)
{
lock (_connectRequests)
{
return _connectRequests.Count > 0;
}
}
else
{
if (_readShutdown)
{
return true;
}
lock (_receiveQueue)
{
return _receiveQueue.Count > 0;
}
}
}
}
public bool Writable => Connected || ProtocolType == ProtocolType.Udp;
public bool Error => false;
public LdnProxySocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, LdnProxy proxy)
{
AddressFamily = addressFamily;
SocketType = socketType;
ProtocolType = protocolType;
_proxy = proxy;
_socketOptions[SocketOptionName.Type] = (int)socketType;
proxy.RegisterSocket(this);
}
private IPEndPoint EnsureLocalEndpoint(bool replace)
{
if (LocalEndPoint != null)
{
if (replace)
{
_proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port);
}
else
{
return (IPEndPoint)LocalEndPoint;
}
}
IPEndPoint localEp = new IPEndPoint(_proxy.LocalAddress, _proxy.GetEphemeralPort(ProtocolType));
LocalEndPoint = localEp;
return localEp;
}
public LdnProxySocket AsAccepted(IPEndPoint remoteEp)
{
Connected = true;
RemoteEndPoint = remoteEp;
IPEndPoint localEp = EnsureLocalEndpoint(true);
_proxy.SignalConnected(localEp, remoteEp, ProtocolType);
return this;
}
private void SignalError(WsaError error)
{
lock (_errors)
{
_errors.Enqueue((int)error);
}
}
private IPEndPoint GetEndpoint(uint ipv4, ushort port)
{
byte[] address = BitConverter.GetBytes(ipv4);
Array.Reverse(address);
return new IPEndPoint(new IPAddress(address), port);
}
public void IncomingData(ProxyDataPacket packet)
{
bool isBroadcast = _proxy.IsBroadcast(packet.Header.Info.DestIpV4);
if (!_closed && (_broadcast || !isBroadcast))
{
lock (_receiveQueue)
{
_receiveQueue.Enqueue(packet);
}
}
}
public ISocketImpl Accept()
{
if (!_isListening)
{
throw new InvalidOperationException();
}
// Accept a pending request to this socket.
lock (_connectRequests)
{
if (!Blocking && _connectRequests.Count == 0)
{
throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
}
}
while (true)
{
bool signalled = _acceptEvent.WaitOne(_acceptTimeout);
lock (_connectRequests)
{
while (_connectRequests.Count > 0)
{
ProxyConnectRequest request = _connectRequests.Dequeue();
if (_connectRequests.Count > 0)
{
_acceptEvent.Set(); // Still more accepts to do.
}
// Is this request made for us?
IPEndPoint endpoint = GetEndpoint(request.Info.DestIpV4, request.Info.DestPort);
if (Equals(endpoint, LocalEndPoint))
{
// Yes - let's accept.
IPEndPoint remoteEndpoint = GetEndpoint(request.Info.SourceIpV4, request.Info.SourcePort);
LdnProxySocket socket = new LdnProxySocket(AddressFamily, SocketType, ProtocolType, _proxy).AsAccepted(remoteEndpoint);
lock (_listenSockets)
{
_listenSockets.Add(socket);
}
return socket;
}
}
}
}
}
public void Bind(EndPoint localEP)
{
if (localEP == null)
{
throw new ArgumentNullException();
}
if (LocalEndPoint != null)
{
_proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port);
}
LocalEndPoint = (IPEndPoint)localEP;
IsBound = true;
}
public void Close()
{
_closed = true;
_proxy.UnregisterSocket(this);
if (Connected)
{
Disconnect(false);
}
lock (_listenSockets)
{
foreach (LdnProxySocket socket in _listenSockets)
{
socket.Close();
}
}
_isListening = false;
}
public void Connect(EndPoint remoteEP)
{
if (_isListening || !IsBound)
{
throw new InvalidOperationException();
}
if (!(remoteEP is IPEndPoint))
{
throw new NotSupportedException();
}
IPEndPoint localEp = EnsureLocalEndpoint(true);
_connecting = true;
_proxy.RequestConnection(localEp, (IPEndPoint)remoteEP, ProtocolType);
if (!Blocking && ProtocolType == ProtocolType.Tcp)
{
throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
}
_connectEvent.WaitOne(); //timeout?
if (_connectResponse.Info.SourceIpV4 == 0)
{
throw new SocketException((int)WsaError.WSAECONNREFUSED);
}
_connectResponse = default;
}
public void HandleConnectResponse(ProxyConnectResponse obj)
{
if (!_connecting)
{
return;
}
_connecting = false;
if (_connectResponse.Info.SourceIpV4 != 0)
{
IPEndPoint remoteEp = GetEndpoint(obj.Info.SourceIpV4, obj.Info.SourcePort);
RemoteEndPoint = remoteEp;
Connected = true;
}
else
{
// Connection failed
SignalError(WsaError.WSAECONNREFUSED);
}
}
public void Disconnect(bool reuseSocket)
{
if (Connected)
{
ConnectionEnded();
// The other side needs to be notified that connection ended.
_proxy.EndConnection(LocalEndPoint as IPEndPoint, RemoteEndPoint as IPEndPoint, ProtocolType);
}
}
private void ConnectionEnded()
{
if (Connected)
{
RemoteEndPoint = null;
Connected = false;
}
}
public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue)
{
if (optionLevel != SocketOptionLevel.Socket)
{
throw new NotImplementedException();
}
if (_socketOptions.TryGetValue(optionName, out int result))
{
byte[] data = BitConverter.GetBytes(result);
Array.Copy(data, 0, optionValue, 0, Math.Min(data.Length, optionValue.Length));
}
else
{
throw new NotImplementedException();
}
}
public void Listen(int backlog)
{
if (!IsBound)
{
throw new SocketException();
}
_isListening = true;
}
public void HandleConnectRequest(ProxyConnectRequest obj)
{
lock (_connectRequests)
{
_connectRequests.Enqueue(obj);
}
_connectEvent.Set();
}
public void HandleDisconnect(ProxyDisconnectMessage message)
{
Disconnect(false);
}
public int Receive(Span<byte> buffer)
{
EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
return ReceiveFrom(buffer, SocketFlags.None, ref dummy);
}
public int Receive(Span<byte> buffer, SocketFlags flags)
{
EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
return ReceiveFrom(buffer, flags, ref dummy);
}
public int Receive(Span<byte> buffer, SocketFlags flags, out SocketError socketError)
{
EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
return ReceiveFrom(buffer, flags, out socketError, ref dummy);
}
public int ReceiveFrom(Span<byte> buffer, SocketFlags flags, ref EndPoint remoteEp)
{
// We just receive all packets meant for us anyways regardless of EP in the actual implementation.
// The point is mostly to return the endpoint that we got the data from.
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
throw new SocketException((int)WsaError.WSAECONNRESET);
}
lock (_receiveQueue)
{
if (_receiveQueue.Count > 0)
{
return ReceiveFromQueue(buffer, flags, ref remoteEp);
}
else if (_readShutdown)
{
return 0;
}
else if (!Blocking)
{
throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
}
}
int timeout = _receiveTimeout;
bool signalled = _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout);
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
throw new SocketException((int)WsaError.WSAECONNRESET);
}
lock (_receiveQueue)
{
if (_receiveQueue.Count > 0)
{
return ReceiveFromQueue(buffer, flags, ref remoteEp);
}
else if (_readShutdown)
{
return 0;
}
else
{
throw new SocketException((int)WsaError.WSAETIMEDOUT);
}
}
}
public int ReceiveFrom(Span<byte> buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp)
{
// We just receive all packets meant for us anyways regardless of EP in the actual implementation.
// The point is mostly to return the endpoint that we got the data from.
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
socketError = SocketError.ConnectionReset;
return -1;
}
lock (_receiveQueue)
{
if (_receiveQueue.Count > 0)
{
return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp);
}
else if (_readShutdown)
{
socketError = SocketError.Success;
return 0;
}
else if (!Blocking)
{
throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
}
}
int timeout = _receiveTimeout;
bool signalled = _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout);
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
throw new SocketException((int)WsaError.WSAECONNRESET);
}
lock (_receiveQueue)
{
if (_receiveQueue.Count > 0)
{
return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp);
}
else if (_readShutdown)
{
socketError = SocketError.Success;
return 0;
}
else
{
socketError = SocketError.TimedOut;
return -1;
}
}
}
private int ReceiveFromQueue(Span<byte> buffer, SocketFlags flags, ref EndPoint remoteEp)
{
int size = buffer.Length;
// Assumes we have the receive queue lock, and at least one item in the queue.
ProxyDataPacket packet = _receiveQueue.Peek();
remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort);
bool peek = (flags & SocketFlags.Peek) != 0;
int read;
if (packet.Data.Length > size)
{
read = size;
// Cannot fit in the output buffer. Copy up to what we've got.
packet.Data.AsSpan(0, size).CopyTo(buffer);
if (ProtocolType == ProtocolType.Udp)
{
// Udp overflows, loses the data, then throws an exception.
if (!peek)
{
_receiveQueue.Dequeue();
}
throw new SocketException((int)WsaError.WSAEMSGSIZE);
}
else if (ProtocolType == ProtocolType.Tcp)
{
// Split the data at the buffer boundary. It will stay on the recieve queue.
byte[] newData = new byte[packet.Data.Length - size];
Array.Copy(packet.Data, size, newData, 0, newData.Length);
packet.Data = newData;
}
}
else
{
read = packet.Data.Length;
packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer);
if (!peek)
{
_receiveQueue.Dequeue();
}
}
return read;
}
private int ReceiveFromQueue(Span<byte> buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp)
{
int size = buffer.Length;
// Assumes we have the receive queue lock, and at least one item in the queue.
ProxyDataPacket packet = _receiveQueue.Peek();
remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort);
bool peek = (flags & SocketFlags.Peek) != 0;
int read;
if (packet.Data.Length > size)
{
read = size;
// Cannot fit in the output buffer. Copy up to what we've got.
packet.Data.AsSpan(0, size).CopyTo(buffer);
if (ProtocolType == ProtocolType.Udp)
{
// Udp overflows, loses the data, then throws an exception.
if (!peek)
{
_receiveQueue.Dequeue();
}
socketError = SocketError.MessageSize;
return -1;
}
else if (ProtocolType == ProtocolType.Tcp)
{
// Split the data at the buffer boundary. It will stay on the recieve queue.
byte[] newData = new byte[packet.Data.Length - size];
Array.Copy(packet.Data, size, newData, 0, newData.Length);
packet.Data = newData;
}
}
else
{
read = packet.Data.Length;
packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer);
if (!peek)
{
_receiveQueue.Dequeue();
}
}
socketError = SocketError.Success;
return read;
}
public int Send(ReadOnlySpan<byte> buffer)
{
// Send to the remote host chosen when we "connect" or "accept".
if (!Connected)
{
throw new SocketException();
}
return SendTo(buffer, SocketFlags.None, RemoteEndPoint);
}
public int Send(ReadOnlySpan<byte> buffer, SocketFlags flags)
{
// Send to the remote host chosen when we "connect" or "accept".
if (!Connected)
{
throw new SocketException();
}
return SendTo(buffer, flags, RemoteEndPoint);
}
public int Send(ReadOnlySpan<byte> buffer, SocketFlags flags, out SocketError socketError)
{
// Send to the remote host chosen when we "connect" or "accept".
if (!Connected)
{
throw new SocketException();
}
return SendTo(buffer, flags, out socketError, RemoteEndPoint);
}
public int SendTo(ReadOnlySpan<byte> buffer, SocketFlags flags, EndPoint remoteEP)
{
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
throw new SocketException((int)WsaError.WSAECONNRESET);
}
IPEndPoint localEp = EnsureLocalEndpoint(false);
if (!(remoteEP is IPEndPoint))
{
throw new NotSupportedException();
}
return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType);
}
public int SendTo(ReadOnlySpan<byte> buffer, SocketFlags flags, out SocketError socketError, EndPoint remoteEP)
{
if (!Connected && ProtocolType == ProtocolType.Tcp)
{
socketError = SocketError.ConnectionReset;
return -1;
}
IPEndPoint localEp = EnsureLocalEndpoint(false);
if (!(remoteEP is IPEndPoint))
{
// throw new NotSupportedException();
socketError = SocketError.OperationNotSupported;
return -1;
}
socketError = SocketError.Success;
return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType);
}
public bool Poll(int microSeconds, SelectMode mode)
{
return mode switch
{
SelectMode.SelectRead => Readable,
SelectMode.SelectWrite => Writable,
SelectMode.SelectError => Error,
_ => false
};
}
public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue)
{
if (optionLevel != SocketOptionLevel.Socket)
{
throw new NotImplementedException();
}
switch (optionName)
{
case SocketOptionName.SendTimeout:
_sendTimeout = optionValue;
break;
case SocketOptionName.ReceiveTimeout:
_receiveTimeout = optionValue;
break;
case SocketOptionName.Broadcast:
_broadcast = optionValue != 0;
break;
}
lock (_socketOptions)
{
_socketOptions[optionName] = optionValue;
}
}
public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue)
{
// Just linger uses this for now in BSD, which we ignore.
}
public void Shutdown(SocketShutdown how)
{
switch (how)
{
case SocketShutdown.Both:
_readShutdown = true;
_writeShutdown = true;
break;
case SocketShutdown.Receive:
_readShutdown = true;
break;
case SocketShutdown.Send:
_writeShutdown = true;
break;
}
}
public void ProxyDestroyed()
{
// Do nothing, for now. Will likely be more useful with TCP.
}
public void Dispose()
{
}
}
}

View file

@ -0,0 +1,93 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using System.Net.Sockets;
using System.Threading;
using TcpClient = NetCoreServer.TcpClient;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
class P2pProxyClient : TcpClient, IProxyClient
{
private const int FailureTimeout = 4000;
public ProxyConfig ProxyConfig { get; private set; }
private RyuLdnProtocol _protocol;
private ManualResetEvent _connected = new ManualResetEvent(false);
private ManualResetEvent _ready = new ManualResetEvent(false);
private AutoResetEvent _error = new AutoResetEvent(false);
public P2pProxyClient(string address, int port) : base(address, port)
{
if (ProxyHelpers.SupportsNoDelay())
{
OptionNoDelay = true;
}
_protocol = new RyuLdnProtocol();
_protocol.ProxyConfig += HandleProxyConfig;
ConnectAsync();
}
protected override void OnConnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client connected a new session with Id {Id}");
_connected.Set();
}
protected override void OnDisconnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client disconnected a session with Id {Id}");
SocketHelpers.UnregisterProxy();
_connected.Reset();
}
protected override void OnReceived(byte[] buffer, long offset, long size)
{
_protocol.Read(buffer, (int)offset, (int)size);
}
protected override void OnError(SocketError error)
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client caught an error with code {error}");
_error.Set();
}
private void HandleProxyConfig(LdnHeader header, ProxyConfig config)
{
ProxyConfig = config;
SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol));
_ready.Set();
}
public bool EnsureProxyReady()
{
return _ready.WaitOne(FailureTimeout);
}
public bool PerformAuth(ExternalProxyConfig config)
{
bool signalled = _connected.WaitOne(FailureTimeout);
if (!signalled)
{
return false;
}
SendAsync(_protocol.Encode(PacketId.ExternalProxy, config));
return true;
}
}
}

View file

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

View file

@ -0,0 +1,90 @@
using NetCoreServer;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
class P2pProxySession : TcpSession
{
public uint VirtualIpAddress { get; private set; }
public RyuLdnProtocol Protocol { get; }
private P2pProxyServer _parent;
private bool _masterClosed;
public P2pProxySession(P2pProxyServer server) : base(server)
{
_parent = server;
Protocol = new RyuLdnProtocol();
Protocol.ProxyDisconnect += HandleProxyDisconnect;
Protocol.ProxyData += HandleProxyData;
Protocol.ProxyConnectReply += HandleProxyConnectReply;
Protocol.ProxyConnect += HandleProxyConnect;
Protocol.ExternalProxy += HandleAuthentication;
}
private void HandleAuthentication(LdnHeader header, ExternalProxyConfig token)
{
if (!_parent.TryRegisterUser(this, token))
{
Disconnect();
}
}
public void SetIpv4(uint ip)
{
VirtualIpAddress = ip;
}
public void DisconnectAndStop()
{
_masterClosed = true;
Disconnect();
}
protected override void OnDisconnected()
{
if (!_masterClosed)
{
_parent.DisconnectProxyClient(this);
}
}
protected override void OnReceived(byte[] buffer, long offset, long size)
{
try
{
Protocol.Read(buffer, (int)offset, (int)size);
}
catch (Exception)
{
Disconnect();
}
}
private void HandleProxyDisconnect(LdnHeader header, ProxyDisconnectMessage message)
{
_parent.HandleProxyDisconnect(this, header, message);
}
private void HandleProxyData(LdnHeader header, ProxyDataHeader message, byte[] data)
{
_parent.HandleProxyData(this, header, message, data);
}
private void HandleProxyConnectReply(LdnHeader header, ProxyConnectResponse data)
{
_parent.HandleProxyConnectReply(this, header, data);
}
private void HandleProxyConnect(LdnHeader header, ProxyConnectRequest message)
{
_parent.HandleProxyConnect(this, header, message);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
using System.Net;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
{
static class ProxyHelpers
{
public static byte[] AddressTo16Byte(IPAddress address)
{
byte[] ipBytes = new byte[16];
byte[] srcBytes = address.GetAddressBytes();
Array.Copy(srcBytes, 0, ipBytes, 0, srcBytes.Length);
return ipBytes;
}
public static bool SupportsNoDelay()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
}
}
}

View file

@ -0,0 +1,380 @@
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
{
class RyuLdnProtocol
{
private const byte CurrentProtocolVersion = 1;
private const int Magic = ('R' << 0) | ('L' << 8) | ('D' << 16) | ('N' << 24);
private const int MaxPacketSize = 131072;
private readonly int _headerSize = Marshal.SizeOf<LdnHeader>();
private byte[] _buffer = new byte[MaxPacketSize];
private int _bufferEnd = 0;
// Client Packets.
public event Action<LdnHeader, InitializeMessage> Initialize;
public event Action<LdnHeader, PassphraseMessage> Passphrase;
public event Action<LdnHeader, NetworkInfo> Connected;
public event Action<LdnHeader, NetworkInfo> SyncNetwork;
public event Action<LdnHeader, NetworkInfo> ScanReply;
public event Action<LdnHeader> ScanReplyEnd;
public event Action<LdnHeader, DisconnectMessage> Disconnected;
// External Proxy Packets.
public event Action<LdnHeader, ExternalProxyConfig> ExternalProxy;
public event Action<LdnHeader, ExternalProxyConnectionState> ExternalProxyState;
public event Action<LdnHeader, ExternalProxyToken> ExternalProxyToken;
// Server Packets.
public event Action<LdnHeader, CreateAccessPointRequest, byte[]> CreateAccessPoint;
public event Action<LdnHeader, CreateAccessPointPrivateRequest, byte[]> CreateAccessPointPrivate;
public event Action<LdnHeader, RejectRequest> Reject;
public event Action<LdnHeader> RejectReply;
public event Action<LdnHeader, SetAcceptPolicyRequest> SetAcceptPolicy;
public event Action<LdnHeader, byte[]> SetAdvertiseData;
public event Action<LdnHeader, ConnectRequest> Connect;
public event Action<LdnHeader, ConnectPrivateRequest> ConnectPrivate;
public event Action<LdnHeader, ScanFilter> Scan;
// Proxy Packets.
public event Action<LdnHeader, ProxyConfig> ProxyConfig;
public event Action<LdnHeader, ProxyConnectRequest> ProxyConnect;
public event Action<LdnHeader, ProxyConnectResponse> ProxyConnectReply;
public event Action<LdnHeader, ProxyDataHeader, byte[]> ProxyData;
public event Action<LdnHeader, ProxyDisconnectMessage> ProxyDisconnect;
// Lifecycle Packets.
public event Action<LdnHeader, NetworkErrorMessage> NetworkError;
public event Action<LdnHeader, PingMessage> Ping;
public RyuLdnProtocol() { }
public void Reset()
{
_bufferEnd = 0;
}
public void Read(byte[] data, int offset, int size)
{
int index = 0;
while (index < size)
{
if (_bufferEnd < _headerSize)
{
// Assemble the header first.
int copyable = Math.Min(size - index, Math.Min(size, _headerSize - _bufferEnd));
Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable);
index += copyable;
_bufferEnd += copyable;
}
if (_bufferEnd >= _headerSize)
{
// The header is available. Make sure we received all the data (size specified in the header)
LdnHeader ldnHeader = MemoryMarshal.Cast<byte, LdnHeader>(_buffer)[0];
if (ldnHeader.Magic != Magic)
{
throw new InvalidOperationException("Invalid magic number in received packet.");
}
if (ldnHeader.Version != CurrentProtocolVersion)
{
throw new InvalidOperationException($"Protocol version mismatch. Expected ${CurrentProtocolVersion}, was ${ldnHeader.Version}.");
}
int finalSize = _headerSize + ldnHeader.DataSize;
if (finalSize >= MaxPacketSize)
{
throw new InvalidOperationException($"Max packet size {MaxPacketSize} exceeded.");
}
int copyable = Math.Min(size - index, Math.Min(size, finalSize - _bufferEnd));
Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable);
index += copyable;
_bufferEnd += copyable;
if (finalSize == _bufferEnd)
{
// The full packet has been retrieved. Send it to be decoded.
byte[] ldnData = new byte[ldnHeader.DataSize];
Array.Copy(_buffer, _headerSize, ldnData, 0, ldnData.Length);
DecodeAndHandle(ldnHeader, ldnData);
Reset();
}
}
}
}
private (T, byte[]) ParseWithData<T>(byte[] data) where T : struct
{
T str = default;
int size = Marshal.SizeOf(str);
byte[] remainder = new byte[data.Length - size];
if (remainder.Length > 0)
{
Array.Copy(data, size, remainder, 0, remainder.Length);
}
return (MemoryMarshal.Read<T>(data), remainder);
}
private void DecodeAndHandle(LdnHeader header, byte[] data)
{
switch ((PacketId)header.Type)
{
// Client Packets.
case PacketId.Initialize:
{
Initialize?.Invoke(header, MemoryMarshal.Read<InitializeMessage>(data));
break;
}
case PacketId.Passphrase:
{
Passphrase?.Invoke(header, MemoryMarshal.Read<PassphraseMessage>(data));
break;
}
case PacketId.Connected:
{
Connected?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
break;
}
case PacketId.SyncNetwork:
{
SyncNetwork?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
break;
}
case PacketId.ScanReply:
{
ScanReply?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
break;
}
case PacketId.ScanReplyEnd:
{
ScanReplyEnd?.Invoke(header);
break;
}
case PacketId.Disconnect:
{
Disconnected?.Invoke(header, MemoryMarshal.Read<DisconnectMessage>(data));
break;
}
// External Proxy Packets.
case PacketId.ExternalProxy:
{
ExternalProxy?.Invoke(header, MemoryMarshal.Read<ExternalProxyConfig>(data));
break;
}
case PacketId.ExternalProxyState:
{
ExternalProxyState?.Invoke(header, MemoryMarshal.Read<ExternalProxyConnectionState>(data));
break;
}
case PacketId.ExternalProxyToken:
{
ExternalProxyToken?.Invoke(header, MemoryMarshal.Read<ExternalProxyToken>(data));
break;
}
// Server Packets.
case PacketId.CreateAccessPoint:
{
(CreateAccessPointRequest packet, byte[] extraData) = ParseWithData<CreateAccessPointRequest>(data);
CreateAccessPoint?.Invoke(header, packet, extraData);
break;
}
case PacketId.CreateAccessPointPrivate:
{
(CreateAccessPointPrivateRequest packet, byte[] extraData) = ParseWithData<CreateAccessPointPrivateRequest>(data);
CreateAccessPointPrivate?.Invoke(header, packet, extraData);
break;
}
case PacketId.Reject:
{
Reject?.Invoke(header, MemoryMarshal.Read<RejectRequest>(data));
break;
}
case PacketId.RejectReply:
{
RejectReply?.Invoke(header);
break;
}
case PacketId.SetAcceptPolicy:
{
SetAcceptPolicy?.Invoke(header, MemoryMarshal.Read<SetAcceptPolicyRequest>(data));
break;
}
case PacketId.SetAdvertiseData:
{
SetAdvertiseData?.Invoke(header, data);
break;
}
case PacketId.Connect:
{
Connect?.Invoke(header, MemoryMarshal.Read<ConnectRequest>(data));
break;
}
case PacketId.ConnectPrivate:
{
ConnectPrivate?.Invoke(header, MemoryMarshal.Read<ConnectPrivateRequest>(data));
break;
}
case PacketId.Scan:
{
Scan?.Invoke(header, MemoryMarshal.Read<ScanFilter>(data));
break;
}
// Proxy Packets
case PacketId.ProxyConfig:
{
ProxyConfig?.Invoke(header, MemoryMarshal.Read<ProxyConfig>(data));
break;
}
case PacketId.ProxyConnect:
{
ProxyConnect?.Invoke(header, MemoryMarshal.Read<ProxyConnectRequest>(data));
break;
}
case PacketId.ProxyConnectReply:
{
ProxyConnectReply?.Invoke(header, MemoryMarshal.Read<ProxyConnectResponse>(data));
break;
}
case PacketId.ProxyData:
{
(ProxyDataHeader packet, byte[] extraData) = ParseWithData<ProxyDataHeader>(data);
ProxyData?.Invoke(header, packet, extraData);
break;
}
case PacketId.ProxyDisconnect:
{
ProxyDisconnect?.Invoke(header, MemoryMarshal.Read<ProxyDisconnectMessage>(data));
break;
}
// Lifecycle Packets.
case PacketId.Ping:
{
Ping?.Invoke(header, MemoryMarshal.Read<PingMessage>(data));
break;
}
case PacketId.NetworkError:
{
NetworkError?.Invoke(header, MemoryMarshal.Read<NetworkErrorMessage>(data));
break;
}
default:
break;
}
}
private static LdnHeader GetHeader(PacketId type, int dataSize)
{
return new LdnHeader()
{
Magic = Magic,
Version = CurrentProtocolVersion,
Type = (byte)type,
DataSize = dataSize
};
}
public byte[] Encode(PacketId type)
{
LdnHeader header = GetHeader(type, 0);
return SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
}
public byte[] Encode(PacketId type, byte[] data)
{
LdnHeader header = GetHeader(type, data.Length);
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
Array.Resize(ref result, result.Length + data.Length);
Array.Copy(data, 0, result, Marshal.SizeOf<LdnHeader>(), data.Length);
return result;
}
public byte[] Encode<T>(PacketId type, T packet) where T : unmanaged
{
byte[] packetData = SpanHelpers.AsSpan<T, byte>(ref packet).ToArray();
LdnHeader header = GetHeader(type, packetData.Length);
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
Array.Resize(ref result, result.Length + packetData.Length);
Array.Copy(packetData, 0, result, Marshal.SizeOf<LdnHeader>(), packetData.Length);
return result;
}
public byte[] Encode<T>(PacketId type, T packet, byte[] data) where T : unmanaged
{
byte[] packetData = SpanHelpers.AsSpan<T, byte>(ref packet).ToArray();
LdnHeader header = GetHeader(type, packetData.Length + data.Length);
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
Array.Resize(ref result, result.Length + packetData.Length + data.Length);
Array.Copy(packetData, 0, result, Marshal.SizeOf<LdnHeader>(), packetData.Length);
Array.Copy(data, 0, result, Marshal.SizeOf<LdnHeader>() + packetData.Length, data.Length);
return result;
}
}
}

View file

@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x4)]
struct DisconnectMessage
{
public uint DisconnectIP;
}
}

View file

@ -0,0 +1,19 @@
using Ryujinx.Common.Memory;
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Sent by the server to point a client towards an external server being used as a proxy.
/// The client then forwards this to the external proxy after connecting, to verify the connection worked.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x26, Pack = 1)]
struct ExternalProxyConfig
{
public Array16<byte> ProxyIp;
public AddressFamily AddressFamily;
public ushort ProxyPort;
public Array16<byte> Token;
}
}

View file

@ -0,0 +1,18 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Indicates a change in connection state for the given client.
/// Is sent to notify the master server when connection is first established.
/// Can be sent by the external proxy to the master server to notify it of a proxy disconnect.
/// Can be sent by the master server to notify the external proxy of a user leaving a room.
/// Both will result in a force kick.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 4)]
struct ExternalProxyConnectionState
{
public uint IpAddress;
public bool Connected;
}
}

View file

@ -0,0 +1,20 @@
using Ryujinx.Common.Memory;
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Sent by the master server to an external proxy to tell them someone is going to connect.
/// This drives authentication, and lets the proxy know what virtual IP to give to each joiner,
/// as these are managed by the master server.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x28)]
struct ExternalProxyToken
{
public uint VirtualIp;
public Array16<byte> Token;
public Array16<byte> PhysicalIp;
public AddressFamily AddressFamily;
}
}

View file

@ -0,0 +1,20 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// This message is first sent by the client to identify themselves.
/// If the server has a token+mac combo that matches the submission, then they are returned their new ID and mac address. (the mac is also reassigned to the new id)
/// Otherwise, they are returned a random mac address.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x16)]
struct InitializeMessage
{
// All 0 if we don't have an ID yet.
public Array16<byte> Id;
// All 0 if we don't have a mac yet.
public Array6<byte> MacAddress;
}
}

View file

@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0xA)]
struct LdnHeader
{
public uint Magic;
public byte Type;
public byte Version;
public int DataSize;
}
}

View file

@ -0,0 +1,36 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
enum PacketId
{
Initialize,
Passphrase,
CreateAccessPoint,
CreateAccessPointPrivate,
ExternalProxy,
ExternalProxyToken,
ExternalProxyState,
SyncNetwork,
Reject,
RejectReply,
Scan,
ScanReply,
ScanReplyEnd,
Connect,
ConnectPrivate,
Connected,
Disconnect,
ProxyConfig,
ProxyConnect,
ProxyConnectReply,
ProxyData,
ProxyDisconnect,
SetAcceptPolicy,
SetAdvertiseData,
Ping = 254,
NetworkError = 255
}
}

View file

@ -0,0 +1,11 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x80)]
struct PassphraseMessage
{
public Array128<byte> Passphrase;
}
}

View file

@ -0,0 +1,11 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x2)]
struct PingMessage
{
public byte Requester;
public byte Id;
}
}

View file

@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
struct ProxyConnectRequest
{
public ProxyInfo Info;
}
}

View file

@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
struct ProxyConnectResponse
{
public ProxyInfo Info;
}
}

View file

@ -0,0 +1,14 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Represents data sent over a transport layer.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x14)]
struct ProxyDataHeader
{
public ProxyInfo Info;
public uint DataLength; // Followed by the data with the specified byte length.
}
}

View file

@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
class ProxyDataPacket
{
public ProxyDataHeader Header;
public byte[] Data;
}
}

View file

@ -0,0 +1,11 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x14)]
struct ProxyDisconnectMessage
{
public ProxyInfo Info;
public int DisconnectReason;
}
}

View file

@ -0,0 +1,20 @@
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Information included in all proxied communication.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 1)]
struct ProxyInfo
{
public uint SourceIpV4;
public ushort SourcePort;
public uint DestIpV4;
public ushort DestPort;
public ProtocolType Protocol;
}
}

View file

@ -0,0 +1,18 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x8)]
struct RejectRequest
{
public uint NodeId;
public DisconnectReason DisconnectReason;
public RejectRequest(DisconnectReason disconnectReason, uint nodeId)
{
DisconnectReason = disconnectReason;
NodeId = nodeId;
}
}
}

View file

@ -0,0 +1,23 @@
using Ryujinx.Common.Memory;
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x28, Pack = 1)]
struct RyuNetworkConfig
{
public Array16<byte> GameVersion;
// PrivateIp is included for external proxies for the case where a client attempts to join from
// their own LAN. UPnP forwarding can fail when connecting devices on the same network over the public IP,
// so if their public IP is identical, the internal address should be sent instead.
// The fields below are 0 if not hosting a p2p proxy.
public Array16<byte> PrivateIp;
public AddressFamily AddressFamily;
public ushort ExternalProxyPort;
public ushort InternalProxyPort;
}
}

View file

@ -0,0 +1,11 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x1, Pack = 1)]
struct SetAcceptPolicyRequest
{
public AcceptPolicy StationAcceptPolicy;
}
}

View file

@ -14,6 +14,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public bool Connected { get; private set; }
public ProxyConfig Config => _parent.NetworkClient.Config;
public Station(IUserLocalCommunicationService parent)
{
_parent = parent;

View file

@ -1,4 +1,5 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
@ -14,5 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
public UserConfig UserConfig;
public NetworkConfig NetworkConfig;
public AddressList AddressList;
public RyuNetworkConfig RyuNetworkConfig;
}
}

View file

@ -1,4 +1,5 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
@ -6,11 +7,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
/// <remarks>
/// Advertise data is appended separately (remaining data in the buffer).
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = 0x94, CharSet = CharSet.Ansi)]
[StructLayout(LayoutKind.Sequential, Size = 0xBC, Pack = 1)]
struct CreateAccessPointRequest
{
public SecurityConfig SecurityConfig;
public UserConfig UserConfig;
public NetworkConfig NetworkConfig;
public RyuNetworkConfig RyuNetworkConfig;
}
}

View file

@ -0,0 +1,11 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x8)]
struct ProxyConfig
{
public uint ProxyIp;
public uint ProxySubnetMask;
}
}

View file

@ -1,6 +1,7 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Nifm.StaticService.Types;
using Ryujinx.Horizon.Common;
using System;
@ -43,7 +44,7 @@ namespace Ryujinx.HLE.HOS.Services.Nifm.StaticService
context.ResponseData.Write((int)requestState);
Logger.Stub?.PrintStub(LogClass.ServiceNifm);
Logger.Stub?.PrintStub(LogClass.ServiceNifm, $"RequestState: {requestState}");
return ResultCode.Success;
}
@ -109,7 +110,9 @@ namespace Ryujinx.HLE.HOS.Services.Nifm.StaticService
// SetConnectionConfirmationOption(i8)
public ResultCode SetConnectionConfirmationOption(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceNifm);
ConnectionConfirmationOption option = (ConnectionConfirmationOption)context.RequestData.ReadByte();
Logger.Stub?.PrintStub(LogClass.ServiceNifm, $"ConnectionConfirmationOption: {option}");
return ResultCode.Success;
}

View file

@ -0,0 +1,12 @@
namespace Ryujinx.HLE.HOS.Services.Nifm.StaticService.Types
{
enum ConnectionConfirmationOption
{
Invalid,
Prohibited,
NotRequired,
Preferred,
Required,
Forced
}
}

View file

@ -0,0 +1,11 @@
namespace Ryujinx.HLE.HOS.Services.Nifm.StaticService.Types
{
enum RequestState
{
Invalid,
Free,
OnHold,
Accepted,
Blocking
}
}

View file

@ -95,10 +95,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
}
}
ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol)
{
Blocking = !creationFlags.HasFlag(BsdSocketCreationFlags.NonBlocking),
};
ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol, context.Device.Configuration.MultiplayerLanInterfaceId);
newBsdSocket.Blocking = !creationFlags.HasFlag(BsdSocketCreationFlags.NonBlocking);
LinuxError errno = LinuxError.SUCCESS;

View file

@ -1,4 +1,5 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
using System;
using System.Collections.Generic;
@ -21,21 +22,21 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; }
public IntPtr Handle => Socket.Handle;
public IntPtr Handle => IntPtr.Zero;
public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint;
public IPEndPoint LocalEndPoint => Socket.LocalEndPoint as IPEndPoint;
public Socket Socket { get; }
public ISocketImpl Socket { get; }
public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType)
public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, string lanInterfaceId)
{
Socket = new Socket(addressFamily, socketType, protocolType);
Socket = SocketHelpers.CreateSocket(addressFamily, socketType, protocolType, lanInterfaceId);
Refcount = 1;
}
private ManagedSocket(Socket socket)
private ManagedSocket(ISocketImpl socket)
{
Socket = socket;
Refcount = 1;
@ -313,7 +314,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported GetSockOpt Option: {option} Level: {level}");
optionValue.Clear();
return LinuxError.SUCCESS;
return LinuxError.EOPNOTSUPP;
}
byte[] tempOptionValue = new byte[optionValue.Length];
@ -347,7 +348,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported SetSockOpt Option: {option} Level: {level}");
return LinuxError.SUCCESS;
return LinuxError.EOPNOTSUPP;
}
int value = optionValue.Length >= 4 ? MemoryMarshal.Read<int>(optionValue) : MemoryMarshal.Read<byte>(optionValue);
@ -493,7 +494,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
try
{
int receiveSize = Socket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
int receiveSize = (Socket as DefaultSocket).BaseSocket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
if (receiveSize > 0)
{
@ -531,7 +532,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
try
{
int sendSize = Socket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
int sendSize = (Socket as DefaultSocket).BaseSocket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
if (sendSize > 0)
{

View file

@ -1,4 +1,5 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
using System.Collections.Generic;
using System.Net.Sockets;
@ -26,45 +27,46 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
public LinuxError Poll(List<PollEvent> events, int timeoutMilliseconds, out int updatedCount)
{
List<Socket> readEvents = new();
List<Socket> writeEvents = new();
List<Socket> errorEvents = new();
List<ISocketImpl> readEvents = new();
List<ISocketImpl> writeEvents = new();
List<ISocketImpl> errorEvents = new();
updatedCount = 0;
foreach (PollEvent evnt in events)
{
ManagedSocket socket = (ManagedSocket)evnt.FileDescriptor;
bool isValidEvent = evnt.Data.InputEvents == 0;
errorEvents.Add(socket.Socket);
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
if (evnt.FileDescriptor is ManagedSocket ms)
{
readEvents.Add(socket.Socket);
bool isValidEvent = evnt.Data.InputEvents == 0;
isValidEvent = true;
}
errorEvents.Add(ms.Socket);
if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0)
{
readEvents.Add(socket.Socket);
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
{
readEvents.Add(ms.Socket);
isValidEvent = true;
}
isValidEvent = true;
}
if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0)
{
writeEvents.Add(socket.Socket);
if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0)
{
readEvents.Add(ms.Socket);
isValidEvent = true;
}
isValidEvent = true;
}
if (!isValidEvent)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}");
return LinuxError.EINVAL;
if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0)
{
writeEvents.Add(ms.Socket);
isValidEvent = true;
}
if (!isValidEvent)
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}");
return LinuxError.EINVAL;
}
}
}
@ -72,7 +74,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
{
int actualTimeoutMicroseconds = timeoutMilliseconds == -1 ? -1 : timeoutMilliseconds * 1000;
Socket.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds);
SocketHelpers.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds);
}
catch (SocketException exception)
{
@ -81,34 +83,37 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
foreach (PollEvent evnt in events)
{
Socket socket = ((ManagedSocket)evnt.FileDescriptor).Socket;
PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents;
if (errorEvents.Contains(socket))
if (evnt.FileDescriptor is ManagedSocket ms)
{
outputEvents |= PollEventTypeMask.Error;
ISocketImpl socket = ms.Socket;
if (!socket.Connected || !socket.IsBound)
PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents;
if (errorEvents.Contains(ms.Socket))
{
outputEvents |= PollEventTypeMask.Disconnected;
}
}
outputEvents |= PollEventTypeMask.Error;
if (readEvents.Contains(socket))
{
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
if (!socket.Connected || !socket.IsBound)
{
outputEvents |= PollEventTypeMask.Disconnected;
}
}
if (readEvents.Contains(ms.Socket))
{
outputEvents |= PollEventTypeMask.Input;
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
{
outputEvents |= PollEventTypeMask.Input;
}
}
}
if (writeEvents.Contains(socket))
{
outputEvents |= PollEventTypeMask.Output;
}
if (writeEvents.Contains(ms.Socket))
{
outputEvents |= PollEventTypeMask.Output;
}
evnt.Data.OutputEvents = outputEvents;
evnt.Data.OutputEvents = outputEvents;
}
}
updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count;
@ -118,53 +123,55 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
public LinuxError Select(List<PollEvent> events, int timeout, out int updatedCount)
{
List<Socket> readEvents = new();
List<Socket> writeEvents = new();
List<Socket> errorEvents = new();
List<ISocketImpl> readEvents = new();
List<ISocketImpl> writeEvents = new();
List<ISocketImpl> errorEvents = new();
updatedCount = 0;
foreach (PollEvent pollEvent in events)
{
ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor;
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input))
if (pollEvent.FileDescriptor is ManagedSocket ms)
{
readEvents.Add(socket.Socket);
}
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input))
{
readEvents.Add(ms.Socket);
}
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output))
{
writeEvents.Add(socket.Socket);
}
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output))
{
writeEvents.Add(ms.Socket);
}
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error))
{
errorEvents.Add(socket.Socket);
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error))
{
errorEvents.Add(ms.Socket);
}
}
}
Socket.Select(readEvents, writeEvents, errorEvents, timeout);
SocketHelpers.Select(readEvents, writeEvents, errorEvents, timeout);
updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count;
foreach (PollEvent pollEvent in events)
{
ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor;
if (readEvents.Contains(socket.Socket))
if (pollEvent.FileDescriptor is ManagedSocket ms)
{
pollEvent.Data.OutputEvents |= PollEventTypeMask.Input;
}
if (readEvents.Contains(ms.Socket))
{
pollEvent.Data.OutputEvents |= PollEventTypeMask.Input;
}
if (writeEvents.Contains(socket.Socket))
{
pollEvent.Data.OutputEvents |= PollEventTypeMask.Output;
}
if (writeEvents.Contains(ms.Socket))
{
pollEvent.Data.OutputEvents |= PollEventTypeMask.Output;
}
if (errorEvents.Contains(socket.Socket))
{
pollEvent.Data.OutputEvents |= PollEventTypeMask.Error;
if (errorEvents.Contains(ms.Socket))
{
pollEvent.Data.OutputEvents |= PollEventTypeMask.Error;
}
}
}

View file

@ -0,0 +1,178 @@
using Ryujinx.Common.Utilities;
using System;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy
{
class DefaultSocket : ISocketImpl
{
public Socket BaseSocket { get; }
public EndPoint RemoteEndPoint => BaseSocket.RemoteEndPoint;
public EndPoint LocalEndPoint => BaseSocket.LocalEndPoint;
public bool Connected => BaseSocket.Connected;
public bool IsBound => BaseSocket.IsBound;
public AddressFamily AddressFamily => BaseSocket.AddressFamily;
public SocketType SocketType => BaseSocket.SocketType;
public ProtocolType ProtocolType => BaseSocket.ProtocolType;
public bool Blocking { get => BaseSocket.Blocking; set => BaseSocket.Blocking = value; }
public int Available => BaseSocket.Available;
private string _lanInterfaceId;
public DefaultSocket(Socket baseSocket, string lanInterfaceId)
{
_lanInterfaceId = lanInterfaceId;
BaseSocket = baseSocket;
}
public DefaultSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId)
{
_lanInterfaceId = lanInterfaceId;
BaseSocket = new Socket(domain, type, protocol);
}
private void EnsureNetworkInterfaceBound()
{
if (_lanInterfaceId != "0" && !BaseSocket.IsBound)
{
(_, UnicastIPAddressInformation ipInfo) = NetworkHelpers.GetLocalInterface(_lanInterfaceId);
BaseSocket.Bind(new IPEndPoint(ipInfo.Address, 0));
}
}
public ISocketImpl Accept()
{
return new DefaultSocket(BaseSocket.Accept(), _lanInterfaceId);
}
public void Bind(EndPoint localEP)
{
// NOTE: The guest is able to receive on 0.0.0.0 without it being limited to the chosen network interface.
// This is because it must get loopback traffic as well. This could allow other network traffic to leak in.
BaseSocket.Bind(localEP);
}
public void Close()
{
BaseSocket.Close();
}
public void Connect(EndPoint remoteEP)
{
EnsureNetworkInterfaceBound();
BaseSocket.Connect(remoteEP);
}
public void Disconnect(bool reuseSocket)
{
BaseSocket.Disconnect(reuseSocket);
}
public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue)
{
BaseSocket.GetSocketOption(optionLevel, optionName, optionValue);
}
public void Listen(int backlog)
{
BaseSocket.Listen(backlog);
}
public int Receive(Span<byte> buffer)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Receive(buffer);
}
public int Receive(Span<byte> buffer, SocketFlags flags)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Receive(buffer, flags);
}
public int Receive(Span<byte> buffer, SocketFlags flags, out SocketError socketError)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Receive(buffer, flags, out socketError);
}
public int ReceiveFrom(Span<byte> buffer, SocketFlags flags, ref EndPoint remoteEP)
{
EnsureNetworkInterfaceBound();
return BaseSocket.ReceiveFrom(buffer, flags, ref remoteEP);
}
public int Send(ReadOnlySpan<byte> buffer)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Send(buffer);
}
public int Send(ReadOnlySpan<byte> buffer, SocketFlags flags)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Send(buffer, flags);
}
public int Send(ReadOnlySpan<byte> buffer, SocketFlags flags, out SocketError socketError)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Send(buffer, flags, out socketError);
}
public int SendTo(ReadOnlySpan<byte> buffer, SocketFlags flags, EndPoint remoteEP)
{
EnsureNetworkInterfaceBound();
return BaseSocket.SendTo(buffer, flags, remoteEP);
}
public bool Poll(int microSeconds, SelectMode mode)
{
return BaseSocket.Poll(microSeconds, mode);
}
public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue)
{
BaseSocket.SetSocketOption(optionLevel, optionName, optionValue);
}
public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue)
{
BaseSocket.SetSocketOption(optionLevel, optionName, optionValue);
}
public void Shutdown(SocketShutdown how)
{
BaseSocket.Shutdown(how);
}
public void Dispose()
{
BaseSocket.Dispose();
}
}
}

View file

@ -0,0 +1,47 @@
using System;
using System.Net;
using System.Net.Sockets;
namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy
{
interface ISocketImpl : IDisposable
{
EndPoint RemoteEndPoint { get; }
EndPoint LocalEndPoint { get; }
bool Connected { get; }
bool IsBound { get; }
AddressFamily AddressFamily { get; }
SocketType SocketType { get; }
ProtocolType ProtocolType { get; }
bool Blocking { get; set; }
int Available { get; }
int Receive(Span<byte> buffer);
int Receive(Span<byte> buffer, SocketFlags flags);
int Receive(Span<byte> buffer, SocketFlags flags, out SocketError socketError);
int ReceiveFrom(Span<byte> buffer, SocketFlags flags, ref EndPoint remoteEP);
int Send(ReadOnlySpan<byte> buffer);
int Send(ReadOnlySpan<byte> buffer, SocketFlags flags);
int Send(ReadOnlySpan<byte> buffer, SocketFlags flags, out SocketError socketError);
int SendTo(ReadOnlySpan<byte> buffer, SocketFlags flags, EndPoint remoteEP);
bool Poll(int microSeconds, SelectMode mode);
ISocketImpl Accept();
void Bind(EndPoint localEP);
void Connect(EndPoint remoteEP);
void Listen(int backlog);
void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue);
void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue);
void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue);
void Shutdown(SocketShutdown how);
void Disconnect(bool reuseSocket);
void Close();
}
}

View file

@ -0,0 +1,71 @@
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy
{
static class SocketHelpers
{
private static LdnProxy _proxy;
public static void Select(List<ISocketImpl> readEvents, List<ISocketImpl> writeEvents, List<ISocketImpl> errorEvents, int timeout)
{
var readDefault = readEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList();
var writeDefault = writeEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList();
var errorDefault = errorEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList();
Socket.Select(readDefault, writeDefault, errorDefault, timeout);
void FilterSockets(List<ISocketImpl> removeFrom, List<Socket> selectedSockets, Func<LdnProxySocket, bool> ldnCheck)
{
removeFrom.RemoveAll(socket =>
{
switch (socket)
{
case DefaultSocket dsocket:
return !selectedSockets.Contains(dsocket.BaseSocket);
case LdnProxySocket psocket:
return !ldnCheck(psocket);
default:
throw new NotImplementedException();
}
});
};
FilterSockets(readEvents, readDefault, (socket) => socket.Readable);
FilterSockets(writeEvents, writeDefault, (socket) => socket.Writable);
FilterSockets(errorEvents, errorDefault, (socket) => socket.Error);
}
public static void RegisterProxy(LdnProxy proxy)
{
if (_proxy != null)
{
UnregisterProxy();
}
_proxy = proxy;
}
public static void UnregisterProxy()
{
_proxy?.Dispose();
_proxy = null;
}
public static ISocketImpl CreateSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId)
{
if (_proxy != null)
{
if (_proxy.Supported(domain, type, protocol))
{
return new LdnProxySocket(domain, type, protocol, _proxy);
}
}
return new DefaultSocket(domain, type, protocol, lanInterfaceId);
}
}
}

View file

@ -1,5 +1,6 @@
using Ryujinx.HLE.HOS.Services.Sockets.Bsd;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using Ryujinx.HLE.HOS.Services.Ssl.Types;
using System;
using System.IO;
@ -116,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService
public ResultCode Handshake(string hostName)
{
StartSslOperation();
_stream = new SslStream(new NetworkStream(((ManagedSocket)Socket).Socket, false), false, null, null);
_stream = new SslStream(new NetworkStream(((DefaultSocket)((ManagedSocket)Socket).Socket).BaseSocket, false), false, null, null);
hostName = RetrieveHostName(hostName);
_stream.AuthenticateAsClient(hostName, null, TranslateSslVersion(_sslVersion), false);
EndSslOperation();

View file

@ -29,6 +29,7 @@
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="NetCoreServer" />
<PackageReference Include="Open.NAT.Core" />
</ItemGroup>
<ItemGroup>

View file

@ -580,7 +580,9 @@ namespace Ryujinx.Headless.SDL2
options.AudioVolume,
options.UseHypervisor ?? true,
options.MultiplayerLanInterfaceId,
Common.Configuration.Multiplayer.MultiplayerMode.Disabled);
Common.Configuration.Multiplayer.MultiplayerMode.Disabled,
false,
"");
return new Switch(configuration);
}

View file

@ -25,6 +25,8 @@ namespace Ryujinx.UI.App.Common
public ulong Id { get; set; }
public string Developer { get; set; } = "Unknown";
public string Version { get; set; } = "0";
public int PlayerCount { get; set; }
public int GameCount { get; set; }
public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; }

View file

@ -10,22 +10,26 @@ using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Npdm;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Ui.Common.App;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Configuration.System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path;
using TimeSpan = System.TimeSpan;
@ -49,6 +53,7 @@ namespace Ryujinx.UI.App.Common
private CancellationTokenSource _cancellationToken;
private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly LdnGameDataSerializerContext LdnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel)
{
@ -487,7 +492,7 @@ namespace Ryujinx.UI.App.Common
controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure();
}
public void LoadApplications(List<string> appDirs)
public async Task LoadApplications(List<string> appDirs)
{
int numApplicationsFound = 0;
int numApplicationsLoaded = 0;
@ -560,6 +565,24 @@ namespace Ryujinx.UI.App.Common
}
}
IEnumerable<LdnGameData> ldnGameDataArray = Array.Empty<LdnGameData>();
if (ConfigurationState.Instance.Multiplayer.Mode == MultiplayerMode.LdnRyu)
{
try
{
using HttpClient httpClient = new HttpClient();
string ldnGameDataArrayString = await httpClient.GetStringAsync("https://ldn.ryujinx.org/api/public_games");
ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, LdnDataSerializerContext.IEnumerableLdnGameData);
}
catch
{
Logger.Warning?.Print(LogClass.Application, "Failed to fetch the public games JSON from the API. Player and game count in the game list will be unavailable.");
}
}
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
foreach (string applicationPath in applicationPaths)
{
@ -572,6 +595,14 @@ namespace Ryujinx.UI.App.Common
{
foreach (var application in applications)
{
if (application.ControlHolder.ByteSpan.Length > 0)
{
IEnumerable<LdnGameData> ldnGameData = ldnGameDataArray.Where(game => application.ControlHolder.Value.LocalCommunicationId.Items.Contains(Convert.ToUInt64(game.TitleId, 16)));
application.PlayerCount = ldnGameData.Sum(game => game.PlayerCount);
application.GameCount = ldnGameData.Count();
}
OnApplicationAdded(new ApplicationAddedEventArgs
{
AppData = application,

View file

@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace Ryujinx.Ui.Common.App
{
public struct LdnGameData
{
public string Id { get; set; }
public int PlayerCount { get; set; }
public int MaxPlayerCount { get; set; }
public string GameName { get; set; }
public string TitleId { get; set; }
public string Mode { get; set; }
public string Status { get; set; }
public IEnumerable<string> Players { get; set; }
}
}

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Ryujinx.Ui.Common.App
{
[JsonSerializable(typeof(IEnumerable<LdnGameData>))]
internal partial class LdnGameDataSerializerContext : JsonSerializerContext
{
}
}

View file

@ -381,6 +381,21 @@ namespace Ryujinx.UI.Common.Configuration
/// </summary>
public string MultiplayerLanInterfaceId { get; set; }
/// <summary>
/// Disable P2p Toggle
/// </summary>
public bool MultiplayerDisableP2p { get; set; }
/// <summary>
/// Default Username
/// </summary>
public string MultiplayerUsername { get; set; }
/// <summary>
/// Local network passphrase, for private networks.
/// </summary>
public string MultiplayerLdnPassphrase { get; set; }
/// <summary>
/// Uses Hypervisor over JIT if available
/// </summary>

View file

@ -30,6 +30,7 @@ namespace Ryujinx.UI.Common.Configuration
public ReactiveObject<bool> AppColumn { get; private set; }
public ReactiveObject<bool> DevColumn { get; private set; }
public ReactiveObject<bool> VersionColumn { get; private set; }
public ReactiveObject<bool> LdnInfoColumn { get; private set; }
public ReactiveObject<bool> TimePlayedColumn { get; private set; }
public ReactiveObject<bool> LastPlayedColumn { get; private set; }
public ReactiveObject<bool> FileExtColumn { get; private set; }
@ -43,6 +44,7 @@ namespace Ryujinx.UI.Common.Configuration
AppColumn = new ReactiveObject<bool>();
DevColumn = new ReactiveObject<bool>();
VersionColumn = new ReactiveObject<bool>();
LdnInfoColumn = new ReactiveObject<bool>();
TimePlayedColumn = new ReactiveObject<bool>();
LastPlayedColumn = new ReactiveObject<bool>();
FileExtColumn = new ReactiveObject<bool>();
@ -569,11 +571,30 @@ namespace Ryujinx.UI.Common.Configuration
/// </summary>
public ReactiveObject<MultiplayerMode> Mode { get; private set; }
/// <summary>
/// Disable P2P Toggle
/// </summary>
public ReactiveObject<bool> DisableP2p { get; private set; }
/// <summary>
/// Default Username
/// </summary>
public ReactiveObject<string> Username { get; private set; }
/// <summary>
/// Local network passphrase, for private networks.
/// </summary>
public ReactiveObject<string> LdnPassphrase { get; private set; }
public MultiplayerSection()
{
LanInterfaceId = new ReactiveObject<string>();
Mode = new ReactiveObject<MultiplayerMode>();
Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode));
DisableP2p = new ReactiveObject<bool>();
DisableP2p.Event += static (_, e) => LogValueChange(e, nameof(DisableP2p));
Username = new ReactiveObject<string>();
LdnPassphrase = new ReactiveObject<string>();
}
}
@ -716,6 +737,7 @@ namespace Ryujinx.UI.Common.Configuration
AppColumn = UI.GuiColumns.AppColumn,
DevColumn = UI.GuiColumns.DevColumn,
VersionColumn = UI.GuiColumns.VersionColumn,
LdnInfoColumn = UI.GuiColumns.LdnInfoColumn,
TimePlayedColumn = UI.GuiColumns.TimePlayedColumn,
LastPlayedColumn = UI.GuiColumns.LastPlayedColumn,
FileExtColumn = UI.GuiColumns.FileExtColumn,
@ -766,6 +788,9 @@ namespace Ryujinx.UI.Common.Configuration
PreferredGpu = Graphics.PreferredGpu,
MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId,
MultiplayerMode = Multiplayer.Mode,
MultiplayerDisableP2p = Multiplayer.DisableP2p,
MultiplayerUsername = Multiplayer.Username,
MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase,
};
return configurationFile;
@ -823,6 +848,9 @@ namespace Ryujinx.UI.Common.Configuration
System.UseHypervisor.Value = true;
Multiplayer.LanInterfaceId.Value = "0";
Multiplayer.Mode.Value = MultiplayerMode.Disabled;
Multiplayer.DisableP2p.Value = false;
Multiplayer.Username.Value = "Player";
Multiplayer.LdnPassphrase.Value = "";
UI.GuiColumns.FavColumn.Value = true;
UI.GuiColumns.IconColumn.Value = true;
UI.GuiColumns.AppColumn.Value = true;
@ -1530,6 +1558,7 @@ namespace Ryujinx.UI.Common.Configuration
UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn;
UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn;
UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn;
UI.GuiColumns.LdnInfoColumn.Value = configurationFileFormat.GuiColumns.LdnInfoColumn;
UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn;
UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn;
UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn;
@ -1572,6 +1601,9 @@ namespace Ryujinx.UI.Common.Configuration
Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId;
Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode;
Multiplayer.DisableP2p.Value = configurationFileFormat.MultiplayerDisableP2p;
Multiplayer.Username.Value = configurationFileFormat.MultiplayerUsername;
Multiplayer.LdnPassphrase.Value = configurationFileFormat.MultiplayerLdnPassphrase;
if (configurationFileUpdated)
{

View file

@ -7,6 +7,7 @@ namespace Ryujinx.UI.Common.Configuration.UI
public bool AppColumn { get; set; }
public bool DevColumn { get; set; }
public bool VersionColumn { get; set; }
public bool LdnInfoColumn { get; set; }
public bool TimePlayedColumn { get; set; }
public bool LastPlayedColumn { get; set; }
public bool FileExtColumn { get; set; }

View file

@ -205,6 +205,8 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
ConfigurationState.Instance.Multiplayer.LdnPassphrase.Event += UpdateLdnPassphraseState;
ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState;
_gpuCancellationTokenSource = new CancellationTokenSource();
_gpuDoneEvent = new ManualResetEvent(false);
@ -489,6 +491,16 @@ namespace Ryujinx.Ava
Device.Configuration.MultiplayerMode = e.NewValue;
}
private void UpdateLdnPassphraseState(object sender, ReactiveEventArgs<string> e)
{
Device.Configuration.MultiplayerLdnPassphrase = e.NewValue;
}
private void UpdateDisableP2pState(object sender, ReactiveEventArgs<bool> e)
{
Device.Configuration.MultiplayerDisableP2p = e.NewValue;
}
public void ToggleVSync()
{
Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
@ -872,7 +884,9 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.System.AudioVolume,
ConfigurationState.Instance.System.UseHypervisor,
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value,
ConfigurationState.Instance.Multiplayer.Mode);
ConfigurationState.Instance.Multiplayer.Mode,
ConfigurationState.Instance.Multiplayer.DisableP2p,
ConfigurationState.Instance.Multiplayer.LdnPassphrase);
Device = new Switch(configuration);
}

View file

@ -781,5 +781,17 @@
"MultiplayerMode": "Mode:",
"MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.",
"MultiplayerModeDisabled": "Disabled",
"MultiplayerModeLdnMitm": "ldn_mitm"
"MultiplayerModeLdnMitm": "ldn_mitm",
"MultiplayerModeLdnRyu": "RyuLDN",
"MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)",
"MultiplayerDisableP2PTooltip": "Disable P2P hosting, instead proxying through the master server instead of directly to the host.",
"LdnPassphrase": "Network Passphrase:",
"LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.",
"LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.",
"LdnPassphraseInputPublic": "(public)",
"GenLdnPass": "Generate Random",
"GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.",
"ClearLdnPass": "Clear",
"ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.",
"InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\""
}

View file

@ -24,6 +24,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
@ -54,6 +55,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public event Action SaveSettingsEvent;
private int _networkInterfaceIndex;
private int _multiplayerModeIndex;
private string _ldnPassphrase;
public int ResolutionScale
{
@ -164,10 +166,24 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool IsVulkanSelected => GraphicsBackendIndex == 0;
public bool UseHypervisor { get; set; }
public bool DisableP2P { get; set; }
public string TimeZone { get; set; }
public string ShaderDumpPath { get; set; }
public string LdnPassphrase
{
get => _ldnPassphrase;
set
{
_ldnPassphrase = value;
IsInvalidLdnPassphraseVisible = !ValidateLdnPassphrase(value);
OnPropertyChanged();
OnPropertyChanged(nameof(IsInvalidLdnPassphraseVisible));
}
}
public int Language { get; set; }
public int Region { get; set; }
public int FsGlobalAccessLogMode { get; set; }
@ -259,6 +275,8 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public bool IsInvalidLdnPassphraseVisible { get; set; }
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
{
_virtualFileSystem = virtualFileSystem;
@ -375,6 +393,11 @@ namespace Ryujinx.Ava.UI.ViewModels
Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(NetworkInterfaceIndex)));
}
private bool ValidateLdnPassphrase(string passphrase)
{
return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && new Regex("Ryujinx-[0-9a-f]{8}").IsMatch(passphrase));
}
public void ValidateAndSetTimeZone(string location)
{
if (_validTzRegions.Contains(location))
@ -473,6 +496,8 @@ namespace Ryujinx.Ava.UI.ViewModels
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value;
DisableP2P = config.Multiplayer.DisableP2p.Value;
LdnPassphrase = config.Multiplayer.LdnPassphrase.Value;
}
public void SaveSettings()
@ -580,6 +605,8 @@ namespace Ryujinx.Ava.UI.ViewModels
config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]];
config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex;
config.Multiplayer.DisableP2p.Value = DisableP2P;
config.Multiplayer.LdnPassphrase.Value = LdnPassphrase;
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);

View file

@ -36,11 +36,57 @@
<ComboBoxItem>
<TextBlock Text="{locale:Locale MultiplayerModeDisabled}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale MultiplayerModeLdnRyu}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale MultiplayerModeLdnMitm}" />
</ComboBoxItem>
</ComboBox>
<CheckBox Margin="10,0,0,0" IsChecked="{Binding DisableP2P}">
<TextBlock Text="{locale:Locale MultiplayerDisableP2P}"
ToolTip.Tip="{locale:Locale MultiplayerDisableP2PTooltip}" />
</CheckBox>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{locale:Locale LdnPassphrase}"
ToolTip.Tip="{locale:Locale LdnPassphraseTooltip}"
Width="200" />
<TextBox Name="LdnPassphrase"
Text="{Binding LdnPassphrase}"
Width="250"
MaxLength="16"
ToolTip.Tip="{locale:Locale LdnPassphraseInputTooltip}"
Watermark="{locale:Locale LdnPassphraseInputPublic}" />
<Button
Name="GenLdnPassButton"
Grid.Column="1"
MinWidth="90"
Margin="10,0,0,0"
ToolTip.Tip="{locale:Locale GenLdnPassTooltip}"
Click="GenLdnPassButton_OnClick">
<TextBlock HorizontalAlignment="Center"
Text="{locale:Locale GenLdnPass}" />
</Button>
<Button
Name="ClearLdnPassButton"
Grid.Column="1"
MinWidth="90"
Margin="10,0,0,0"
ToolTip.Tip="{locale:Locale ClearLdnPassTooltip}"
Click="ClearLdnPassButton_OnClick">
<TextBlock HorizontalAlignment="Center"
Text="{locale:Locale ClearLdnPass}" />
</Button>
</StackPanel>
<TextBlock Margin="10,0,0,0"
VerticalAlignment="Center"
Name="InvalidLdnPassphraseBlock"
FontStyle="Italic"
IsVisible="{Binding IsInvalidLdnPassphraseVisible}"
Focusable="False"
Text="{locale:Locale InvalidLdnPassphrase}" />
<Separator Height="1" />
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabNetworkConnection}" />
<CheckBox Margin="10,0,0,0" IsChecked="{Binding EnableInternetAccess}">

View file

@ -1,12 +1,29 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Ryujinx.Ava.UI.ViewModels;
using System;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsNetworkView : UserControl
{
public SettingsViewModel ViewModel;
public SettingsNetworkView()
{
InitializeComponent();
}
private void GenLdnPassButton_OnClick(object sender, RoutedEventArgs e)
{
byte[] code = new byte[4];
new Random().NextBytes(code);
ViewModel.LdnPassphrase = $"Ryujinx-{BitConverter.ToUInt32(code):x8}";
}
private void ClearLdnPassButton_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.LdnPassphrase = "";
}
}
}

View file

@ -638,10 +638,10 @@ namespace Ryujinx.Ava.UI.Windows
_isLoading = true;
Thread applicationLibraryThread = new(() =>
Thread applicationLibraryThread = new(async () =>
{
ApplicationLibrary.DesiredLanguage = ConfigurationState.Instance.System.Language;
ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs);
await ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs);
_isLoading = false;
})

View file

@ -82,6 +82,7 @@ namespace Ryujinx.Ava.UI.Windows
NavPanel.Content = AudioPage;
break;
case "NetworkPage":
NetworkPage.ViewModel = ViewModel;
NavPanel.Content = NetworkPage;
break;
case "LoggingPage":