TESTERS WANTED: RyuLDN implementation (#65)

These changes allow players to matchmake for local wireless using a LDN
server. The network implementation originates from Berry's public TCP
RyuLDN fork. Logo and unrelated changes have been removed.

Additionally displays LDN game status in the game selection window when
RyuLDN is enabled.

Functionality is only enabled while network mode is set to "RyuLDN" in
the settings.
This commit is contained in:
Vudjun
2024-11-11 22:06:50 +00:00
committed by GitHub
parent abfcfcaf0f
commit 6d8738c048
93 changed files with 4100 additions and 189 deletions

View File

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

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 nint Handle => Socket.Handle;
public nint 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;
@@ -185,6 +186,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
}
}
bool hasEmittedBlockingWarning = false;
public LinuxError Receive(out int receiveSize, Span<byte> buffer, BsdSocketFlags flags)
{
LinuxError result;
@@ -199,6 +202,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
shouldBlockAfterOperation = true;
}
if (Blocking && !hasEmittedBlockingWarning)
{
Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors.");
hasEmittedBlockingWarning = true;
}
receiveSize = Socket.Receive(buffer, ConvertBsdSocketFlags(flags));
result = LinuxError.SUCCESS;
@@ -236,6 +245,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
shouldBlockAfterOperation = true;
}
if (Blocking && !hasEmittedBlockingWarning)
{
Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors.");
hasEmittedBlockingWarning = true;
}
if (!Socket.IsBound)
{
receiveSize = -1;
@@ -313,7 +328,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 +362,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 +508,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 +546,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 readonly 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,74 @@
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();
if (readDefault.Count != 0 || writeDefault.Count != 0 || errorDefault.Count != 0)
{
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

@@ -292,7 +292,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres
{
string host = MemoryHelper.ReadAsciiString(context.Memory, inputBufferPosition, (int)inputBufferSize);
if (!context.Device.Configuration.EnableInternetAccess)
if (host != "localhost" && !context.Device.Configuration.EnableInternetAccess)
{
Logger.Info?.Print(LogClass.ServiceSfdnsres, $"Guest network access disabled, DNS Blocked: {host}");