// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.Client.Library; using System; using System.Collections.Generic; using System.Diagnostics; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using MUnique.OpenMU.Network; using MUnique.OpenMU.Network.SimpleModulus; using MUnique.OpenMU.Network.Xor; using Pipelines.Sockets.Unofficial; /// /// Class which manages the connections which are created through the game client. /// To identify each connection, we use handles (a simple number). /// public unsafe partial class ConnectionManager { /// /// The currently active connections, with their handle as key. /// private static readonly Dictionary Connections = new(); /// /// The currently used maximum handle number. /// private static int _maxHandle; /// /// Connects the specified host and port. /// /// The pointer to a string which contains the host (ip or hostname). /// The port. /// /// A flag, if the connection is supposed to be encrypted. /// This is usually 1 for connections to the game server, but 0 for connections to the connect server. /// /// The pointer to an unmanaged method which is called when a new packet got received. /// Parameters: handle, size, pointer to the data. /// The pointer to an unmanaged method which is called when the connection got disconnected. /// Parameter: handle. /// /// The handle of the created connection. If negative, the connection couldn't be established. /// [UnmanagedCallersOnly(EntryPoint = "ConnectionManager_Connect")] public static int Connect(IntPtr hostPtr, int port, byte isEncrypted, delegate* unmanaged onPacketReceived, delegate* unmanaged onDisconnected) { try { var host = NativeInterop.PtrToWideString(hostPtr) ?? throw new ArgumentNullException(nameof(hostPtr)); return ConnectInner(host, port, isEncrypted == 1, onPacketReceived, onDisconnected); } catch (Exception ex) { Debug.WriteLine($"Error establishing connection: {ex}"); return -1; } } /// /// Sends a packet over the connection of the specified handle. /// /// The handle of the connection. /// The pointer to the packet data. /// The count of bytes which should be sent. [UnmanagedCallersOnly(EntryPoint = "ConnectionManager_Send")] public static void Send(int handle, byte* data, int count) { if (Connections.TryGetValue(handle, out var connection)) { try { var bytes = new Span(data, count); bytes.SetPacketSize(); connection.Send(bytes); Debug.WriteLine("Sent {0} bytes with handle {1}", count, handle); } catch (Exception ex) { // A send failure means the connection is broken. Tear it down so // the Disconnected event fires and the client can auto-reconnect, // instead of silently swallowing the error and looking online. Debug.WriteLine($"Error sending {count} bytes with handle {handle}: {ex}"); connection.DisconnectAndDispose(); } } else { Debug.WriteLine("Connection with handle {0} not found.", handle); } } /// /// Begins receiving data for the connection of the specified handle. /// /// The handle of the connection. [UnmanagedCallersOnly(EntryPoint = "ConnectionManager_BeginReceive")] public static void BeginReceive(int connectionHandle) { if (Connections.TryGetValue(connectionHandle, out var connection)) { connection.BeginReceive(); } } /// /// Disconnects the connection of the specified handle. /// /// The handle of the connection. [UnmanagedCallersOnly(EntryPoint = "ConnectionManager_Disconnect")] public static void Disconnect(int connectionHandle) { if (Connections.TryGetValue(connectionHandle, out var connection)) { connection.DisconnectAndDispose(); } } private static int ConnectInner(string host, int port, bool isEncrypted, delegate* unmanaged onPacketReceived, delegate* unmanaged onDisconnected) { var tcpClient = new TcpClient(host, port); ConfigureKeepAlive(tcpClient.Client); var socketConnection = SocketConnection.Create(tcpClient.Client); var encryptor = isEncrypted ? new PipelinedXor32Encryptor(new PipelinedSimpleModulusEncryptor(socketConnection.Output, PipelinedSimpleModulusEncryptor.DefaultClientKey).Writer) : null; var decryptor = isEncrypted ? new PipelinedSimpleModulusDecryptor(socketConnection.Input, PipelinedSimpleModulusDecryptor.DefaultClientKey) : null; var connection = new Connection(socketConnection, decryptor, encryptor, new NullLogger()); var handle = Interlocked.Increment(ref _maxHandle); var wrapper = new ConnectionWrapper(handle, connection, onPacketReceived, onDisconnected); Connections.Add(handle, wrapper); connection.Disconnected += () => { Connections.Remove(handle); return ValueTask.CompletedTask; }; return handle; } /// /// Idle time, in seconds, before the OS starts sending TCP keep-alive probes. /// private const int KeepAliveIdleSeconds = 4; /// /// Interval, in seconds, between TCP keep-alive probes. /// private const int KeepAliveIntervalSeconds = 2; /// /// Number of unanswered keep-alive probes before the connection is dropped. /// private const int KeepAliveRetryCount = 3; /// /// Enables aggressive TCP keep-alive so the OS detects dead or half-open /// connections (server crash, network drop) within ~10 seconds and tears the /// socket down. That makes the receive loop fault and raise /// , which the game client polls to /// trigger auto-reconnect. Without this, a peer that vanishes without sending /// FIN/RST keeps the connection alive for minutes. Probes are answered by the /// healthy peer's OS automatically, so this never drops a live connection. /// /// The connected socket to configure. private static void ConfigureKeepAlive(Socket socket) { try { socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); } catch (Exception ex) { Debug.WriteLine($"Failed to enable SO_KEEPALIVE: {ex}"); } // Modern cross-platform options (.NET 5+). Set each independently so an // unsupported one doesn't skip the others (notably the retry count, which // older Windows ignores). TrySetSocketOption(socket, SocketOptionName.TcpKeepAliveTime, KeepAliveIdleSeconds); TrySetSocketOption(socket, SocketOptionName.TcpKeepAliveInterval, KeepAliveIntervalSeconds); TrySetSocketOption(socket, SocketOptionName.TcpKeepAliveRetryCount, KeepAliveRetryCount); // Windows fallback/override: SIO_KEEPALIVE_VALS reliably sets the idle // time and probe interval (in milliseconds) where the socket options // above are sometimes ignored. try { if (OperatingSystem.IsWindows()) { var values = new byte[12]; BitConverter.GetBytes(1u).CopyTo(values, 0); // on BitConverter.GetBytes((uint)(KeepAliveIdleSeconds * 1000)).CopyTo(values, 4); // idle (ms) BitConverter.GetBytes((uint)(KeepAliveIntervalSeconds * 1000)).CopyTo(values, 8); // interval (ms) socket.IOControl(IOControlCode.KeepAliveValues, values, null); } } catch (Exception ex) { Debug.WriteLine($"Failed to configure keep-alive via IOControl: {ex}"); } } private static void TrySetSocketOption(Socket socket, SocketOptionName option, int value) { try { socket.SetSocketOption(SocketOptionLevel.Tcp, option, value); } catch (Exception ex) { Debug.WriteLine($"Failed to set socket option {option}: {ex}"); } } }