Files
Ryujinx-greemdev/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs
TSRBerry 2989c163a8 editorconfig: Set default encoding to UTF-8 (#5793)
* editorconfig: Add default charset

* Change file encoding from UTF-8-BOM to UTF-8
2023-12-04 14:17:13 +01:00

612 lines
19 KiB
C#

using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
{
internal class LanDiscovery : IDisposable
{
private const int DefaultPort = 11452;
private const ushort CommonChannel = 6;
private const byte CommonLinkLevel = 3;
private const byte CommonNetworkType = 2;
private const int FailureTimeout = 4000;
private readonly LdnMitmClient _parent;
private readonly LanProtocol _protocol;
private bool _initialized;
private readonly Ssid _fakeSsid;
private ILdnTcpSocket _tcp;
private LdnProxyUdpServer _udp, _udp2;
private readonly List<LdnProxyTcpSession> _stations = new();
private readonly object _lock = new();
private readonly AutoResetEvent _apConnected = new(false);
internal readonly IPAddress LocalAddr;
internal readonly IPAddress LocalBroadcastAddr;
internal NetworkInfo NetworkInfo;
public bool IsHost => _tcp is LdnProxyTcpServer;
private readonly Random _random = new();
// NOTE: Credit to https://stackoverflow.com/a/39338188
private static IPAddress GetBroadcastAddress(IPAddress address, IPAddress mask)
{
uint ipAddress = BitConverter.ToUInt32(address.GetAddressBytes(), 0);
uint ipMaskV4 = BitConverter.ToUInt32(mask.GetAddressBytes(), 0);
uint broadCastIpAddress = ipAddress | ~ipMaskV4;
return new IPAddress(BitConverter.GetBytes(broadCastIpAddress));
}
private static NetworkInfo GetEmptyNetworkInfo()
{
NetworkInfo networkInfo = new()
{
NetworkId = new NetworkId
{
SessionId = new Array16<byte>(),
},
Common = new CommonNetworkInfo
{
MacAddress = new Array6<byte>(),
Ssid = new Ssid
{
Name = new Array33<byte>(),
},
},
Ldn = new LdnNetworkInfo
{
NodeCountMax = LdnConst.NodeCountMax,
SecurityParameter = new Array16<byte>(),
Nodes = new Array8<NodeInfo>(),
AdvertiseData = new Array384<byte>(),
Reserved4 = new Array140<byte>(),
},
};
for (int i = 0; i < LdnConst.NodeCountMax; i++)
{
networkInfo.Ldn.Nodes[i] = new NodeInfo
{
MacAddress = new Array6<byte>(),
UserName = new Array33<byte>(),
Reserved2 = new Array16<byte>(),
};
}
return networkInfo;
}
public LanDiscovery(LdnMitmClient parent, IPAddress ipAddress, IPAddress ipv4Mask)
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initialize LanDiscovery using IP: {ipAddress}");
_parent = parent;
LocalAddr = ipAddress;
LocalBroadcastAddr = GetBroadcastAddress(ipAddress, ipv4Mask);
_fakeSsid = new Ssid
{
Length = LdnConst.SsidLengthMax,
};
_random.NextBytes(_fakeSsid.Name.AsSpan()[..32]);
_protocol = new LanProtocol(this);
_protocol.Accept += OnConnect;
_protocol.SyncNetwork += OnSyncNetwork;
_protocol.DisconnectStation += DisconnectStation;
NetworkInfo = GetEmptyNetworkInfo();
ResetStations();
if (!InitUdp())
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Initialize: InitUdp failed.");
return;
}
_initialized = true;
}
protected void OnSyncNetwork(NetworkInfo info)
{
bool updated = false;
lock (_lock)
{
if (!NetworkInfo.Equals(info))
{
NetworkInfo = info;
updated = true;
Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"Host IP: {NetworkHelpers.ConvertUint(info.Ldn.Nodes[0].Ipv4Address)}");
}
}
if (updated)
{
_parent.InvokeNetworkChange(info, true);
}
_apConnected.Set();
}
protected void OnConnect(LdnProxyTcpSession station)
{
lock (_lock)
{
station.NodeId = LocateEmptyNode();
if (_stations.Count > LdnConst.StationCountMax || station.NodeId == -1)
{
station.Disconnect();
station.Dispose();
return;
}
_stations.Add(station);
UpdateNodes();
}
}
public void DisconnectStation(LdnProxyTcpSession station)
{
if (!station.IsDisposed)
{
if (station.IsConnected)
{
station.Disconnect();
}
station.Dispose();
}
lock (_lock)
{
if (_stations.Remove(station))
{
NetworkInfo.Ldn.Nodes[station.NodeId] = new NodeInfo()
{
MacAddress = new Array6<byte>(),
UserName = new Array33<byte>(),
Reserved2 = new Array16<byte>(),
};
UpdateNodes();
}
}
}
public bool SetAdvertiseData(byte[] data)
{
if (data.Length > LdnConst.AdvertiseDataSizeMax)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "AdvertiseData exceeds size limit.");
return false;
}
data.CopyTo(NetworkInfo.Ldn.AdvertiseData.AsSpan());
NetworkInfo.Ldn.AdvertiseDataSize = (ushort)data.Length;
// NOTE: Otherwise this results in SessionKeepFailed or MasterDisconnected
lock (_lock)
{
if (NetworkInfo.Ldn.Nodes[0].IsConnected == 1)
{
UpdateNodes(true);
}
}
return true;
}
public void InitNetworkInfo()
{
lock (_lock)
{
NetworkInfo.Common.MacAddress = GetFakeMac();
NetworkInfo.Common.Channel = CommonChannel;
NetworkInfo.Common.LinkLevel = CommonLinkLevel;
NetworkInfo.Common.NetworkType = CommonNetworkType;
NetworkInfo.Common.Ssid = _fakeSsid;
NetworkInfo.Ldn.Nodes = new Array8<NodeInfo>();
for (int i = 0; i < LdnConst.NodeCountMax; i++)
{
NetworkInfo.Ldn.Nodes[i].NodeId = (byte)i;
NetworkInfo.Ldn.Nodes[i].IsConnected = 0;
}
}
}
protected Array6<byte> GetFakeMac(IPAddress address = null)
{
address ??= LocalAddr;
byte[] ip = address.GetAddressBytes();
var macAddress = new Array6<byte>();
new byte[] { 0x02, 0x00, ip[0], ip[1], ip[2], ip[3] }.CopyTo(macAddress.AsSpan());
return macAddress;
}
public bool InitTcp(bool listening, IPAddress address = null, int port = DefaultPort)
{
Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"LanDiscovery InitTcp: IP: {address}, listening: {listening}");
if (_tcp != null)
{
_tcp.DisconnectAndStop();
_tcp.Dispose();
_tcp = null;
}
ILdnTcpSocket tcpSocket;
if (listening)
{
try
{
address ??= LocalAddr;
tcpSocket = new LdnProxyTcpServer(_protocol, address, port);
}
catch (Exception ex)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpServer: {ex}");
return false;
}
if (!tcpSocket.Start())
{
return false;
}
}
else
{
if (address == null)
{
return false;
}
try
{
tcpSocket = new LdnProxyTcpClient(_protocol, address, port);
}
catch (Exception ex)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpClient: {ex}");
return false;
}
}
_tcp = tcpSocket;
return true;
}
public bool InitUdp()
{
_udp?.Stop();
_udp2?.Stop();
try
{
// NOTE: Linux won't receive any broadcast packets if the socket is not bound to the broadcast address.
// Windows only works if bound to localhost or the local address.
// See this discussion: https://stackoverflow.com/questions/13666789/receiving-udp-broadcast-packets-on-linux
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
_udp2 = new LdnProxyUdpServer(_protocol, LocalBroadcastAddr, DefaultPort);
}
_udp = new LdnProxyUdpServer(_protocol, LocalAddr, DefaultPort);
}
catch (Exception ex)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyUdpServer: {ex}");
return false;
}
return true;
}
public NetworkInfo[] Scan(ushort channel, ScanFilter filter)
{
_udp.ClearScanResults();
if (_protocol.SendBroadcast(_udp, LanPacketType.Scan, DefaultPort) < 0)
{
return Array.Empty<NetworkInfo>();
}
List<NetworkInfo> outNetworkInfo = new();
foreach (KeyValuePair<ulong, NetworkInfo> item in _udp.GetScanResults())
{
bool copy = true;
if (filter.Flag.HasFlag(ScanFilterFlag.LocalCommunicationId))
{
copy &= filter.NetworkId.IntentId.LocalCommunicationId == item.Value.NetworkId.IntentId.LocalCommunicationId;
}
if (filter.Flag.HasFlag(ScanFilterFlag.SessionId))
{
copy &= filter.NetworkId.SessionId.AsSpan().SequenceEqual(item.Value.NetworkId.SessionId.AsSpan());
}
if (filter.Flag.HasFlag(ScanFilterFlag.NetworkType))
{
copy &= filter.NetworkType == (NetworkType)item.Value.Common.NetworkType;
}
if (filter.Flag.HasFlag(ScanFilterFlag.Ssid))
{
Span<byte> gameSsid = item.Value.Common.Ssid.Name.AsSpan()[item.Value.Common.Ssid.Length..];
Span<byte> scanSsid = filter.Ssid.Name.AsSpan()[filter.Ssid.Length..];
copy &= gameSsid.SequenceEqual(scanSsid);
}
if (filter.Flag.HasFlag(ScanFilterFlag.SceneId))
{
copy &= filter.NetworkId.IntentId.SceneId == item.Value.NetworkId.IntentId.SceneId;
}
if (copy)
{
if (item.Value.Ldn.Nodes[0].UserName[0] != 0)
{
outNetworkInfo.Add(item.Value);
}
else
{
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Scan: Got empty Username. There might be a timing issue somewhere...");
}
}
}
return outNetworkInfo.ToArray();
}
protected void ResetStations()
{
lock (_lock)
{
foreach (LdnProxyTcpSession station in _stations)
{
station.Disconnect();
station.Dispose();
}
_stations.Clear();
}
}
private int LocateEmptyNode()
{
Array8<NodeInfo> nodes = NetworkInfo.Ldn.Nodes;
for (int i = 1; i < nodes.Length; i++)
{
if (nodes[i].IsConnected == 0)
{
return i;
}
}
return -1;
}
protected void UpdateNodes(bool forceUpdate = false)
{
int countConnected = 1;
foreach (LdnProxyTcpSession station in _stations.Where(station => station.IsConnected))
{
countConnected++;
station.OverrideInfo();
// NOTE: This is not part of the original implementation.
NetworkInfo.Ldn.Nodes[station.NodeId] = station.NodeInfo;
}
byte nodeCount = (byte)countConnected;
bool networkInfoChanged = forceUpdate || NetworkInfo.Ldn.NodeCount != nodeCount;
NetworkInfo.Ldn.NodeCount = nodeCount;
foreach (LdnProxyTcpSession station in _stations)
{
if (station.IsConnected)
{
if (_protocol.SendPacket(station, LanPacketType.SyncNetwork, SpanHelpers.AsSpan<NetworkInfo, byte>(ref NetworkInfo).ToArray()) < 0)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to send {LanPacketType.SyncNetwork} to station {station.NodeId}");
}
}
}
if (networkInfoChanged)
{
_parent.InvokeNetworkChange(NetworkInfo, true);
}
}
protected NodeInfo GetNodeInfo(NodeInfo node, UserConfig userConfig, ushort localCommunicationVersion)
{
uint ipAddress = NetworkHelpers.ConvertIpv4Address(LocalAddr);
node.MacAddress = GetFakeMac();
node.IsConnected = 1;
node.UserName = userConfig.UserName;
node.LocalCommunicationVersion = localCommunicationVersion;
node.Ipv4Address = ipAddress;
return node;
}
public bool CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig)
{
if (!InitTcp(true))
{
return false;
}
InitNetworkInfo();
NetworkInfo.Ldn.NodeCountMax = networkConfig.NodeCountMax;
NetworkInfo.Ldn.SecurityMode = (ushort)securityConfig.SecurityMode;
NetworkInfo.Common.Channel = networkConfig.Channel == 0 ? (ushort)6 : networkConfig.Channel;
NetworkInfo.NetworkId.SessionId = new Array16<byte>();
_random.NextBytes(NetworkInfo.NetworkId.SessionId.AsSpan());
NetworkInfo.NetworkId.IntentId = networkConfig.IntentId;
NetworkInfo.Ldn.Nodes[0] = GetNodeInfo(NetworkInfo.Ldn.Nodes[0], userConfig, networkConfig.LocalCommunicationVersion);
NetworkInfo.Ldn.Nodes[0].IsConnected = 1;
NetworkInfo.Ldn.NodeCount++;
_parent.InvokeNetworkChange(NetworkInfo, true);
return true;
}
public void DestroyNetwork()
{
if (_tcp != null)
{
try
{
_tcp.DisconnectAndStop();
}
finally
{
_tcp.Dispose();
_tcp = null;
}
}
ResetStations();
}
public NetworkError Connect(NetworkInfo networkInfo, UserConfig userConfig, uint localCommunicationVersion)
{
_apConnected.Reset();
if (networkInfo.Ldn.NodeCount == 0)
{
return NetworkError.Unknown;
}
IPAddress address = NetworkHelpers.ConvertUint(networkInfo.Ldn.Nodes[0].Ipv4Address);
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Connecting to host: {address}");
if (!InitTcp(false, address))
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Could not initialize TCPClient");
return NetworkError.ConnectNotFound;
}
if (!_tcp.Connect())
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Failed to connect.");
return NetworkError.ConnectFailure;
}
NodeInfo myNode = GetNodeInfo(new NodeInfo(), userConfig, (ushort)localCommunicationVersion);
if (_protocol.SendPacket(_tcp, LanPacketType.Connect, SpanHelpers.AsSpan<NodeInfo, byte>(ref myNode).ToArray()) < 0)
{
return NetworkError.Unknown;
}
return _apConnected.WaitOne(FailureTimeout) ? NetworkError.None : NetworkError.ConnectTimeout;
}
public void Dispose()
{
if (_initialized)
{
DisconnectAndStop();
ResetStations();
_initialized = false;
}
_protocol.Accept -= OnConnect;
_protocol.SyncNetwork -= OnSyncNetwork;
_protocol.DisconnectStation -= DisconnectStation;
}
public void DisconnectAndStop()
{
if (_udp != null)
{
try
{
_udp.Stop();
}
finally
{
_udp.Dispose();
_udp = null;
}
}
if (_udp2 != null)
{
try
{
_udp2.Stop();
}
finally
{
_udp2.Dispose();
_udp2 = null;
}
}
if (_tcp != null)
{
try
{
_tcp.DisconnectAndStop();
}
finally
{
_tcp.Dispose();
_tcp = null;
}
}
}
}
}