/************************************************************************* * ModernUO * * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: ForeignProtocol.cs * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * *************************************************************************/ using System; namespace Server.Network; public enum ForeignProtocolKind { None, Http, Tls, Ssh } public enum ForeignProtocolMatch { None, /// A prefix matched, but more bytes are needed to be sure. Incomplete, Confirmed } /// /// Identifies traffic that is positively some OTHER protocol (HTTP, TLS, SSH), rather than deciding whether /// a connection is a good Ultima Online client. /// /// /// The direction matters. A legitimate client with encryption enabled when the shard expects none sends a /// structurally correct connection whose payload is noise, because LoginEncryption.ClientDecrypt is a /// byte-for-byte stream XOR: length survives, content does not. So "unreadable" cannot mean "hostile", while /// "speaks HTTP" safely can. Nothing here assumes arrival framing; see /// dev-docs/ip-bans-and-allowlists.md. /// public static class ForeignProtocol { private const int RequiredBytes = 8; private const int MaxTlsRecordLength = 16384; public static ForeignProtocolMatch Identify(ReadOnlySpan buffer, out ForeignProtocolKind kind) { kind = ForeignProtocolKind.None; // Too little to match a prefix; the caller's own short-read handling covers it. if (buffer.Length < 4) { return ForeignProtocolMatch.None; } var candidate = MatchPrefix(buffer); if (candidate == ForeignProtocolKind.None) { return ForeignProtocolMatch.None; } if (buffer.Length < RequiredBytes) { return ForeignProtocolMatch.Incomplete; } if (!Confirm(buffer, candidate)) { return ForeignProtocolMatch.None; } kind = candidate; return ForeignProtocolMatch.Confirmed; } private static ForeignProtocolKind MatchPrefix(ReadOnlySpan buffer) { // TLS handshake record: content type 0x16, major version 3, minor version 0..4 (SSL 3.0 - TLS 1.3). if (buffer[0] == 0x16 && buffer[1] == 0x03 && buffer[2] <= 0x04) { return ForeignProtocolKind.Tls; } if (StartsWith(buffer, "SSH-")) { return ForeignProtocolKind.Ssh; } // HTTP request methods. Four bytes only selects a candidate; Confirm checks the request line. if (StartsWith(buffer, "GET ") || StartsWith(buffer, "POST") || StartsWith(buffer, "HEAD") || StartsWith(buffer, "PUT ") || StartsWith(buffer, "OPTI") || StartsWith(buffer, "DELE") || StartsWith(buffer, "CONN") || StartsWith(buffer, "TRAC") || StartsWith(buffer, "PATC")) { return ForeignProtocolKind.Http; } return ForeignProtocolKind.None; } private static bool Confirm(ReadOnlySpan buffer, ForeignProtocolKind candidate) { if (candidate == ForeignProtocolKind.Tls) { // A seed can collide with the 0x16 0x03 0x0? prefix (that is just the address 22.3.x.x), so // require a ClientHello inside a plausible record. var recordLength = (buffer[3] << 8) | buffer[4]; return buffer[5] == 0x01 && recordLength is >= 4 and <= MaxTlsRecordLength; } // A UO client's fifth byte is a packet id (0x80, 0x91, 0xEF), none of them printable, so requiring // the request line to continue in ASCII lets a seed that spells "GET " fall through. for (var i = 4; i < buffer.Length && i < 16; i++) { if (!IsPrintableAscii(buffer[i])) { return false; } } return true; } private static bool IsPrintableAscii(byte value) => value is >= 0x20 and <= 0x7E or (byte)'\r' or (byte)'\n' or (byte)'\t'; private static bool StartsWith(ReadOnlySpan buffer, string ascii) { for (var i = 0; i < ascii.Length; i++) { if (buffer[i] != (byte)ascii[i]) { return false; } } return true; } }