Decompiled source of FischywebFusionLayer v1.0.2
FischywebFusionLayer.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Threading; using System.Threading.Tasks; using FischywebFusionLayer; using FischywebNetworker.Shared; using FischywebNetworker.Shared.Crypto; using LabFusion.Data; using LabFusion.Network; using LabFusion.Network.Fischyweb; using LabFusion.Player; using LabFusion.Senders; using LabFusion.Support; using LabFusion.UI.Popups; using LabFusion.Utilities; using LabFusion.Voice; using LabFusion.Voice.Unity; using MelonLoader; using MelonLoader.Utils; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(FischywebLayerMod), "Fischyweb Fusion Layer", "1.0.2", "FischyWeb", null)] [assembly: MelonGame("Stress Level Zero", "BONELAB")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("FischywebFusionLayer")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.2.0")] [assembly: AssemblyInformationalVersion("1.0.2")] [assembly: AssemblyProduct("FischywebFusionLayer")] [assembly: AssemblyTitle("FischywebFusionLayer")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.2.0")] [module: UnverifiableCode] namespace FischywebNetworker.Shared { public static class MiniJson { public sealed class JsonException : Exception { public JsonException(string message) : base(message) { } } private sealed class Parser { private readonly string _s; private int _i; public bool End => _i >= _s.Length; public Parser(string s) { _s = s; } public JsonException Error(string msg) { int num = 1; int num2 = 1; for (int i = 0; i < _i && i < _s.Length; i++) { if (_s[i] == '\n') { num++; num2 = 1; } else { num2++; } } return new JsonException($"{msg} at line {num}, column {num2}"); } public void SkipWs() { while (_i < _s.Length) { switch (_s[_i]) { case '\t': case '\n': case '\r': case ' ': case '\ufeff': _i++; break; case '/': if (_i + 1 < _s.Length && _s[_i + 1] == '/') { while (_i < _s.Length && _s[_i] != '\n') { _i++; } break; } return; default: return; } } } public object ParseValue(int depth) { if (depth > 32) { throw Error("nesting too deep"); } if (End) { throw Error("unexpected end of input"); } char c = _s[_i]; switch (c) { case '{': return ParseObj(depth); case '[': return ParseArr(depth); case '"': return ParseStr(); case 't': Expect("true"); return true; case 'f': Expect("false"); return false; case 'n': Expect("null"); return null; default: switch (c) { case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return ParseNum(); default: throw Error($"unexpected character '{c}'"); } } } private void Expect(string word) { if (string.CompareOrdinal(_s, _i, word, 0, word.Length) != 0) { throw Error("expected '" + word + "'"); } _i += word.Length; } private Dictionary<string, object> ParseObj(int depth) { Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal); _i++; SkipWs(); if (!End && _s[_i] == '}') { _i++; return dictionary; } while (true) { SkipWs(); if (End || _s[_i] != '"') { throw Error("expected property name"); } string text = ParseStr(); SkipWs(); if (End || _s[_i] != ':') { throw Error("expected ':'"); } _i++; SkipWs(); if (dictionary.ContainsKey(text)) { throw Error("duplicate key '" + text + "'"); } dictionary[text] = ParseValue(depth + 1); SkipWs(); if (End) { throw Error("unterminated object"); } if (_s[_i] != ',') { break; } _i++; SkipWs(); if (!End && _s[_i] == '}') { _i++; return dictionary; } } if (_s[_i] == '}') { _i++; return dictionary; } throw Error("expected ',' or '}'"); } private List<object> ParseArr(int depth) { List<object> list = new List<object>(); _i++; SkipWs(); if (!End && _s[_i] == ']') { _i++; return list; } while (true) { SkipWs(); list.Add(ParseValue(depth + 1)); SkipWs(); if (End) { throw Error("unterminated array"); } if (_s[_i] != ',') { break; } _i++; } if (_s[_i] == ']') { _i++; return list; } throw Error("expected ',' or ']'"); } private string ParseStr() { StringBuilder stringBuilder = new StringBuilder(); _i++; while (true) { if (End) { throw Error("unterminated string"); } char c = _s[_i++]; if (c == '"') { return stringBuilder.ToString(); } if (c == '\\') { if (End) { throw Error("bad escape"); } switch (_s[_i++]) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': if (_i + 4 > _s.Length) { throw Error("bad unicode escape"); } stringBuilder.Append((char)int.Parse(_s.Substring(_i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture)); _i += 4; break; default: throw Error("bad escape"); } } else { if (c < ' ') { break; } stringBuilder.Append(c); } } throw Error("control character in string"); } private double ParseNum() { int i = _i; if (_s[_i] == '-') { _i++; } while (_i < _s.Length && "0123456789.eE+-".IndexOf(_s[_i]) >= 0) { _i++; } if (!double.TryParse(_s.Substring(i, _i - i), NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { throw Error("invalid number"); } return result; } } public static object Parse(string text) { Parser parser = new Parser(text ?? ""); parser.SkipWs(); object result = parser.ParseValue(0); parser.SkipWs(); if (!parser.End) { throw parser.Error("unexpected trailing characters"); } return result; } public static Dictionary<string, object> ParseObject(string text) { return (Parse(text) as Dictionary<string, object>) ?? throw new JsonException("top-level value must be an object"); } public static string Serialize(object value, bool indent = true) { StringBuilder stringBuilder = new StringBuilder(); Write(stringBuilder, value, indent, 0); if (indent) { stringBuilder.Append('\n'); } return stringBuilder.ToString(); } private static void Write(StringBuilder sb, object v, bool indent, int level) { if (v != null) { if (!(v is bool flag)) { if (!(v is string s)) { if (!(v is int num)) { if (!(v is long num2)) { if (!(v is float num3)) { if (!(v is double num4)) { if (!(v is IDictionary<string, object> dictionary)) { if (v is IEnumerable enumerable) { sb.Append('['); bool flag2 = true; foreach (object item in enumerable) { if (!flag2) { sb.Append(indent ? ", " : ","); } flag2 = false; Write(sb, item, indent, level + 1); } sb.Append(']'); } else { WriteString(sb, Convert.ToString(v, CultureInfo.InvariantCulture)); } return; } sb.Append('{'); bool flag3 = true; foreach (KeyValuePair<string, object> item2 in dictionary) { if (!flag3) { sb.Append(','); } flag3 = false; if (indent) { sb.Append('\n'); sb.Append(' ', (level + 1) * 2); } WriteString(sb, item2.Key); sb.Append(indent ? ": " : ":"); Write(sb, item2.Value, indent, level + 1); } if (indent && dictionary.Count > 0) { sb.Append('\n'); sb.Append(' ', level * 2); } sb.Append('}'); } else { sb.Append(num4.ToString("R", CultureInfo.InvariantCulture)); } } else { sb.Append(((double)num3).ToString("R", CultureInfo.InvariantCulture)); } } else { sb.Append(num2.ToString(CultureInfo.InvariantCulture)); } } else { sb.Append(num.ToString(CultureInfo.InvariantCulture)); } } else { WriteString(sb, s); } } else { sb.Append(flag ? "true" : "false"); } } else { sb.Append("null"); } } private static void WriteString(StringBuilder sb, string s) { sb.Append('"'); foreach (char c in s) { switch (c) { case '"': sb.Append("\\\""); continue; case '\\': sb.Append("\\\\"); continue; case '\n': sb.Append("\\n"); continue; case '\r': sb.Append("\\r"); continue; case '\t': sb.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder = sb.Append("\\u"); int num = c; stringBuilder.Append(num.ToString("x4")); } else { sb.Append(c); } } sb.Append('"'); } } public sealed class JsonSection { private readonly Dictionary<string, object> _d; private readonly string _path; public readonly List<string> Errors; public IEnumerable<string> Keys => _d.Keys; public JsonSection(Dictionary<string, object> d, string path, List<string> errors) { _d = d ?? new Dictionary<string, object>(); _path = path; Errors = errors; } public bool Has(string key) { return _d.ContainsKey(key); } public JsonSection Section(string key) { if (_d.TryGetValue(key, out var value) && value is Dictionary<string, object> d) { return new JsonSection(d, Path(key), Errors); } if (_d.ContainsKey(key) && !(_d[key] is Dictionary<string, object>)) { Errors.Add(Path(key) + " must be an object"); } return new JsonSection(null, Path(key), Errors); } private string Path(string key) { if (!string.IsNullOrEmpty(_path)) { return _path + "." + key; } return key; } public string String(string key, string def, int maxLength = 512) { if (!_d.TryGetValue(key, out var value) || value == null) { return def; } if (value is string text) { if (text.Length > maxLength) { Errors.Add($"{Path(key)} is longer than {maxLength} characters"); return def; } return text; } Errors.Add(Path(key) + " must be a string"); return def; } public bool Bool(string key, bool def) { if (!_d.TryGetValue(key, out var value) || value == null) { return def; } if (value is bool) { return (bool)value; } Errors.Add(Path(key) + " must be true or false"); return def; } public int Int(string key, int def, int min, int max) { if (!_d.TryGetValue(key, out var value) || value == null) { return def; } if (value is double num && Math.Abs(num - Math.Round(num)) < 1E-09) { if (num < (double)min || num > (double)max) { Errors.Add($"{Path(key)} must be between {min} and {max} (was {num})"); return def; } return (int)num; } Errors.Add(Path(key) + " must be a whole number"); return def; } public double Number(string key, double def, double min, double max) { if (!_d.TryGetValue(key, out var value) || value == null) { return def; } if (value is double num) { if (num < min || num > max) { Errors.Add($"{Path(key)} must be between {min} and {max} (was {num})"); return def; } return num; } Errors.Add(Path(key) + " must be a number"); return def; } public List<string> StringList(string key) { List<string> list = new List<string>(); if (!_d.TryGetValue(key, out var value) || value == null) { return list; } if (value is List<object> list2) { { foreach (object item2 in list2) { if (item2 is string item) { list.Add(item); } else { Errors.Add(Path(key) + " must contain only strings"); } } return list; } } Errors.Add(Path(key) + " must be an array"); return list; } } public static class FrameIO { public static async Task<(byte[] Prefix, byte[] Body)> ReadFrameAsync(Stream stream, int maxLength, CancellationToken ct) { byte[] prefix = new byte[4]; await ReadExactlyAsync(stream, prefix, ct).ConfigureAwait(continueOnCapturedContext: false); int num = prefix[0] | (prefix[1] << 8) | (prefix[2] << 16) | (prefix[3] << 24); if (num == 0 || num > maxLength) { throw new ProtocolException($"frame length {num} out of range"); } byte[] body = new byte[num]; await ReadExactlyAsync(stream, body, ct).ConfigureAwait(continueOnCapturedContext: false); return (Prefix: prefix, Body: body); } public static async Task ReadExactlyAsync(Stream stream, byte[] buffer, CancellationToken ct) { int num; for (int read = 0; read < buffer.Length; read += num) { num = await stream.ReadAsync(buffer, read, buffer.Length - read, ct).ConfigureAwait(continueOnCapturedContext: false); if (num <= 0) { throw new EndOfStreamException("connection closed"); } } } public static byte[] PlainFrame(ReadOnlySpan<byte> body) { if (body.Length == 0) { throw new ArgumentException("bad frame length"); } byte[] array = new byte[body.Length + 4]; array[0] = (byte)body.Length; array[1] = (byte)(body.Length >> 8); array[2] = (byte)(body.Length >> 16); array[3] = (byte)(body.Length >> 24); body.CopyTo(array.AsSpan(4)); return array; } } public static class MonoClock { private static readonly Stopwatch Watch = Stopwatch.StartNew(); public static long Milliseconds => Watch.ElapsedMilliseconds; public static double Seconds => Watch.Elapsed.TotalSeconds; public static uint WireMs => (uint)Watch.ElapsedMilliseconds; public static int Diff(uint a, uint b) { return (int)(a - b); } } public sealed class TokenBucket { private readonly double _ratePerSecond; private readonly double _burst; private double _tokens; private long _lastMs; public TokenBucket(double ratePerSecond, double burst) { _ratePerSecond = ratePerSecond; _burst = burst; _tokens = burst; _lastMs = MonoClock.Milliseconds; } public bool TryTake(double cost = 1.0, long nowMs = -1L) { if (nowMs < 0) { nowMs = MonoClock.Milliseconds; } double num = (double)(nowMs - _lastMs) / 1000.0; _lastMs = nowMs; if (num > 0.0) { _tokens = Math.Min(_burst, _tokens + num * _ratePerSecond); } if (_tokens < cost) { return false; } _tokens -= cost; return true; } } public static class NameRules { public static string Sanitize(string name) { if (string.IsNullOrEmpty(name)) { return ""; } StringBuilder stringBuilder = new StringBuilder(name.Length); bool flag = true; foreach (char c in name) { if (char.IsControl(c) || char.IsSurrogate(c) || char.GetUnicodeCategory(c) == UnicodeCategory.Format || c == '<' || c == '>' || c == '{' || c == '}') { continue; } if (char.IsWhiteSpace(c)) { if (!flag) { stringBuilder.Append(' '); } flag = true; continue; } stringBuilder.Append(c); flag = false; if (stringBuilder.Length >= 32) { break; } } return stringBuilder.ToString().Trim(); } } public static class Handshake { public const int ClientHelloSize = 39; public const int ServerHelloSize = 87; public static byte[] BuildClientHello(ReadOnlySpan<byte> clientEphemeralPublic) { NetWriter netWriter = new NetWriter(39); netWriter.WriteByte(1); netWriter.WriteUInt32(1380865862u); netWriter.WriteUInt16(1); netWriter.WriteBytes(clientEphemeralPublic); return netWriter.ToArray(); } public static bool TryParseClientHello(ReadOnlySpan<byte> frame, out ushort version, out byte[] clientEphemeralPublic) { NetReader netReader = new NetReader(frame); if (netReader.ReadByte() != 1) { throw new ProtocolException("expected ClientHello"); } if (netReader.ReadUInt32() != 1380865862) { throw new ProtocolException("bad magic"); } version = netReader.ReadUInt16(); if (version != 1) { clientEphemeralPublic = null; return false; } clientEphemeralPublic = netReader.ReadBytes(32).ToArray(); netReader.ExpectEnd(); return true; } public static byte[] BuildServerHello(ReadOnlySpan<byte> serverStaticPublic, ReadOnlySpan<byte> serverEphemeralPublic, ReadOnlySpan<byte> confirmTag) { NetWriter netWriter = new NetWriter(87); netWriter.WriteByte(2); netWriter.WriteUInt32(1380865862u); netWriter.WriteUInt16(1); netWriter.WriteBytes(serverStaticPublic); netWriter.WriteBytes(serverEphemeralPublic); netWriter.WriteBytes(confirmTag); return netWriter.ToArray(); } public static void ParseServerHello(ReadOnlySpan<byte> frame, out byte[] serverStaticPublic, out byte[] serverEphemeralPublic, out byte[] confirmTag) { NetReader netReader = new NetReader(frame); if (netReader.ReadByte() != 2) { throw new ProtocolException("expected ServerHello"); } if (netReader.ReadUInt32() != 1380865862) { throw new ProtocolException("bad magic"); } if (netReader.ReadUInt16() != 1) { throw new ProtocolException("version mismatch"); } serverStaticPublic = netReader.ReadBytes(32).ToArray(); serverEphemeralPublic = netReader.ReadBytes(32).ToArray(); confirmTag = netReader.ReadBytes(16).ToArray(); netReader.ExpectEnd(); } public static byte[] BuildReject(DisconnectReason reason, string text) { NetWriter netWriter = new NetWriter(64); netWriter.WriteByte(3); netWriter.WriteUInt32(1380865862u); netWriter.WriteUInt16(1); netWriter.WriteByte((byte)reason); netWriter.WriteShortString(text ?? "", 200); return netWriter.ToArray(); } public static void ParseReject(ReadOnlySpan<byte> frame, out ushort serverVersion, out DisconnectReason reason, out string text) { NetReader netReader = new NetReader(frame); if (netReader.ReadByte() != 3) { throw new ProtocolException("expected ServerReject"); } if (netReader.ReadUInt32() != 1380865862) { throw new ProtocolException("bad magic"); } serverVersion = netReader.ReadUInt16(); reason = (DisconnectReason)netReader.ReadByte(); text = netReader.ReadShortString(200); } public static byte[] TranscriptHash(ReadOnlySpan<byte> clientHello, ReadOnlySpan<byte> serverStaticPublic, ReadOnlySpan<byte> serverEphemeralPublic) { Sha256 sha = new Sha256(); sha.Update(Encoding.ASCII.GetBytes("FWN relay handshake v1")); sha.Update(clientHello); sha.Update(serverStaticPublic); sha.Update(serverEphemeralPublic); byte[] array = new byte[32]; sha.Final(array); return array; } public static SessionKeys DeriveKeys(ReadOnlySpan<byte> transcriptHash, ReadOnlySpan<byte> dhEs, ReadOnlySpan<byte> dhEe) { byte[] array = new byte[64]; dhEs.CopyTo(array); dhEe.CopyTo(array.AsSpan(32)); byte[] array2 = Hkdf.DeriveKey(transcriptHash, array, Encoding.ASCII.GetBytes("FWN relay keys v1"), 160); CryptoUtil.Zero(array); SessionKeys result = new SessionKeys(array2); CryptoUtil.Zero(array2); return result; } public static byte[] ComputeConfirmTag(SessionKeys keys, ReadOnlySpan<byte> transcriptHash) { FischywebNetworker.Shared.Crypto.ChaCha20Poly1305 chaCha20Poly = new FischywebNetworker.Shared.Crypto.ChaCha20Poly1305(keys.Confirm); byte[] array = new byte[16]; chaCha20Poly.Encrypt(new byte[12], ReadOnlySpan<byte>.Empty, Span<byte>.Empty, array, transcriptHash); return array; } public static SessionKeys ServerRespond(ReadOnlySpan<byte> clientHello, ReadOnlySpan<byte> clientEphemeralPublic, ReadOnlySpan<byte> serverStaticPrivate, ReadOnlySpan<byte> serverStaticPublic, out byte[] serverHello) { byte[] array = X25519.GeneratePrivateKey(); byte[] array2 = X25519.PublicKeyFromPrivate(array); byte[] array3 = new byte[32]; byte[] array4 = new byte[32]; try { if (!X25519.SharedSecret(array3, serverStaticPrivate, clientEphemeralPublic) || !X25519.SharedSecret(array4, array, clientEphemeralPublic)) { throw new ProtocolException("degenerate client key"); } byte[] array5 = TranscriptHash(clientHello, serverStaticPublic, array2); SessionKeys sessionKeys = DeriveKeys(array5, array3, array4); serverHello = BuildServerHello(serverStaticPublic, array2, ComputeConfirmTag(sessionKeys, array5)); return sessionKeys; } finally { CryptoUtil.Zero(array); CryptoUtil.Zero(array3); CryptoUtil.Zero(array4); } } public static SessionKeys ClientFinish(ReadOnlySpan<byte> clientHello, ReadOnlySpan<byte> clientEphemeralPrivate, ReadOnlySpan<byte> serverStaticPublic, ReadOnlySpan<byte> serverEphemeralPublic, ReadOnlySpan<byte> confirmTag) { byte[] array = new byte[32]; byte[] array2 = new byte[32]; try { if (!X25519.SharedSecret(array, clientEphemeralPrivate, serverStaticPublic) || !X25519.SharedSecret(array2, clientEphemeralPrivate, serverEphemeralPublic)) { throw new ProtocolException("degenerate server key"); } byte[] array3 = TranscriptHash(clientHello, serverStaticPublic, serverEphemeralPublic); SessionKeys sessionKeys = DeriveKeys(array3, array, array2); if (!CryptoUtil.FixedTimeEquals(ComputeConfirmTag(sessionKeys, array3), confirmTag)) { sessionKeys.Dispose(); throw new ProtocolException("server failed key confirmation"); } return sessionKeys; } finally { CryptoUtil.Zero(array); CryptoUtil.Zero(array2); } } } public sealed class SessionKeys : IDisposable { public readonly byte[] Confirm = new byte[32]; public readonly byte[] TcpClientToServer = new byte[32]; public readonly byte[] TcpServerToClient = new byte[32]; public readonly byte[] UdpClientToServer = new byte[32]; public readonly byte[] UdpServerToClient = new byte[32]; public SessionKeys(ReadOnlySpan<byte> okm) { okm.Slice(0, 32).CopyTo(Confirm); okm.Slice(32, 32).CopyTo(TcpClientToServer); okm.Slice(64, 32).CopyTo(TcpServerToClient); okm.Slice(96, 32).CopyTo(UdpClientToServer); okm.Slice(128, 32).CopyTo(UdpServerToClient); } public void Dispose() { CryptoUtil.Zero(Confirm); CryptoUtil.Zero(TcpClientToServer); CryptoUtil.Zero(TcpServerToClient); CryptoUtil.Zero(UdpClientToServer); CryptoUtil.Zero(UdpServerToClient); } } public sealed class TcpCipher { private readonly FischywebNetworker.Shared.Crypto.ChaCha20Poly1305 _aead; private ulong _counter; public TcpCipher(byte[] key) { _aead = new FischywebNetworker.Shared.Crypto.ChaCha20Poly1305(key); } public byte[] Seal(ReadOnlySpan<byte> plaintext) { int num = plaintext.Length + 16; if (num > 1048832) { throw new InvalidOperationException("TCP message too large"); } byte[] array = new byte[4 + num]; array[0] = (byte)num; array[1] = (byte)(num >> 8); array[2] = (byte)(num >> 16); array[3] = (byte)(num >> 24); Span<byte> span = stackalloc byte[12]; NonceFromCounter(span, _counter++); _aead.Encrypt(span, plaintext, array.AsSpan(4, plaintext.Length), array.AsSpan(4 + plaintext.Length, 16), array.AsSpan(0, 4)); return array; } public byte[] Open(ReadOnlySpan<byte> lengthPrefix, ReadOnlySpan<byte> body) { if (body.Length < 16) { return null; } byte[] array = new byte[body.Length - 16]; Span<byte> span = stackalloc byte[12]; NonceFromCounter(span, _counter); if (!_aead.Decrypt(span, body.Slice(0, array.Length), body.Slice(array.Length, 16), array, lengthPrefix)) { return null; } _counter++; return array; } internal static void NonceFromCounter(Span<byte> nonce, ulong counter) { nonce.Slice(0, 4).Clear(); for (int i = 0; i < 8; i++) { nonce[4 + i] = (byte)(counter >> 8 * i); } } } public sealed class UdpCipher { public const byte DataKind = 16; private readonly FischywebNetworker.Shared.Crypto.ChaCha20Poly1305 _sendAead; private readonly FischywebNetworker.Shared.Crypto.ChaCha20Poly1305 _recvAead; private ulong _sendCounter; public readonly ReplayWindow Replay = new ReplayWindow(); public ulong LastSentPacketNumber => _sendCounter; public UdpCipher(byte[] sendKey, byte[] receiveKey) { _sendAead = new FischywebNetworker.Shared.Crypto.ChaCha20Poly1305(sendKey); _recvAead = new FischywebNetworker.Shared.Crypto.ChaCha20Poly1305(receiveKey); } public static bool TryPeekConnectionId(ReadOnlySpan<byte> datagram, out uint connectionId) { connectionId = 0u; if (datagram.Length < 29 || datagram.Length > 1200) { return false; } if (datagram[0] != 16) { return false; } connectionId = (uint)(datagram[1] | (datagram[2] << 8) | (datagram[3] << 16) | (datagram[4] << 24)); return connectionId != 0; } public int Seal(uint connectionId, ReadOnlySpan<byte> plaintext, byte[] output) { int num = 13 + plaintext.Length + 16; if (num > 1200 || output.Length < num) { throw new InvalidOperationException("datagram too large"); } ulong num2 = ++_sendCounter; output[0] = 16; output[1] = (byte)connectionId; output[2] = (byte)(connectionId >> 8); output[3] = (byte)(connectionId >> 16); output[4] = (byte)(connectionId >> 24); for (int i = 0; i < 8; i++) { output[5 + i] = (byte)(num2 >> 8 * i); } Span<byte> span = stackalloc byte[12]; TcpCipher.NonceFromCounter(span, num2); _sendAead.Encrypt(span, plaintext, output.AsSpan(13, plaintext.Length), output.AsSpan(13 + plaintext.Length, 16), output.AsSpan(0, 13)); return num; } public int Open(ReadOnlySpan<byte> datagram, Span<byte> plaintext, out ulong packetNumber) { packetNumber = 0uL; if (datagram.Length < 29 || datagram.Length > 1200) { return -1; } ulong num = 0uL; for (int i = 0; i < 8; i++) { num |= (ulong)datagram[5 + i] << 8 * i; } if (num == 0L || !Replay.CanAccept(num)) { return -1; } int num2 = datagram.Length - 13 - 16; Span<byte> span = stackalloc byte[12]; TcpCipher.NonceFromCounter(span, num); if (!_recvAead.Decrypt(span, datagram.Slice(13, num2), datagram.Slice(13 + num2, 16), plaintext, datagram.Slice(0, 13))) { return -1; } Replay.Mark(num); packetNumber = num; return num2; } } public sealed class ReplayWindow { private const int WindowBits = 1024; private readonly ulong[] _bitmap = new ulong[16]; private ulong _highest; public ulong Highest => _highest; public bool CanAccept(ulong pn) { if (pn == 0L) { return false; } if (pn > _highest) { return true; } if (_highest - pn >= 1024) { return false; } int num = (int)(pn % 1024); return (_bitmap[num >> 6] & (ulong)(1L << num)) == 0; } public void Mark(ulong pn) { if (pn > _highest) { if (pn - _highest >= 1024) { Array.Clear(_bitmap, 0, _bitmap.Length); } else { for (ulong num = _highest + 1; num < pn; num++) { int num2 = (int)(num % 1024); _bitmap[num2 >> 6] &= (ulong)(~(1L << num2)); } } _highest = pn; } int num3 = (int)(pn % 1024); _bitmap[num3 >> 6] |= (ulong)(1L << num3); } } public static class Protocol { public const uint Magic = 1380865862u; public const ushort Version = 1; public const string ProductName = "Fischyweb Networker"; public const string RelayVersion = "1.0.0"; public const string DefaultHostname = "sync.fischyweb.com"; public const int DefaultTcpPort = 17777; public const int DefaultUdpPort = 17778; public const string HandshakeLabel = "FWN relay handshake v1"; public const string KeyScheduleLabel = "FWN relay keys v1"; public const string IdentityLabel = "FWN player id v1"; } public static class ProtocolLimits { public const int MaxPreHandshakeFrame = 512; public const int MaxMessage = 1048576; public const int MaxTcpFrame = 1048832; public const int MaxTcpPlaintext = 1048816; public const int MaxUdpDatagram = 1200; public const int UdpHeaderSize = 13; public const int MaxUdpPlaintext = 1171; public const int MaxUnreliablePayload = 1158; public const int MaxUsernameChars = 32; public const int MaxUsernameBytes = 96; public const int MaxMetadataKeyBytes = 128; public const int MaxMetadataValueBytes = 61440; public const int MaxMetadataTotalBytes = 262144; public const int MaxMetadataKeys = 64; public const int SecretSize = 32; } public enum HandshakeFrame : byte { ClientHello = 1, ServerHello, ServerReject } public enum DisconnectReason : byte { None = 0, ProtocolMismatch = 1, ServerFull = 2, Banned = 5, ServerShuttingDown = 6, RateLimited = 7, Kicked = 8, Timeout = 9, ProtocolError = 10, ReplacedBySession = 11, ServerError = 12, ClientLeft = 13, HandshakeFailed = 14, TooManyConnections = 15, HostLeft = 20, LobbyNotFound = 21, LobbyFull = 22, KickedByHost = 23 } public static class DisconnectReasonText { public static string Describe(DisconnectReason r) { return r switch { DisconnectReason.ProtocolMismatch => "Version mismatch - update Fischyweb Networker", DisconnectReason.ServerFull => "The Fischyweb server is full", DisconnectReason.Banned => "You are banned from this Fischyweb server", DisconnectReason.ServerShuttingDown => "The Fischyweb server is restarting", DisconnectReason.RateLimited => "Too much traffic - slow down", DisconnectReason.Kicked => "Kicked by the server administrator", DisconnectReason.Timeout => "Connection timed out", DisconnectReason.ProtocolError => "Protocol error", DisconnectReason.ReplacedBySession => "Logged in from another game", DisconnectReason.ServerError => "Server error", DisconnectReason.ClientLeft => "Left", DisconnectReason.HandshakeFailed => "Secure handshake failed", DisconnectReason.TooManyConnections => "Too many connections from your network", DisconnectReason.HostLeft => "The host closed the server", DisconnectReason.LobbyNotFound => "That server no longer exists", DisconnectReason.LobbyFull => "That server is full", DisconnectReason.KickedByHost => "Kicked by the host", _ => "Disconnected", }; } public static bool IsRetryable(DisconnectReason r) { if (r != DisconnectReason.None && r != DisconnectReason.Timeout && r != DisconnectReason.ServerShuttingDown && r != DisconnectReason.ServerError && r != DisconnectReason.RateLimited && r != DisconnectReason.TooManyConnections) { return r == DisconnectReason.ServerFull; } return true; } } public enum RelayMessage : byte { Login = 1, Ping = 2, StartServer = 16, SetLobbyMetadata = 17, RequestLobbies = 18, JoinServer = 19, Leave = 20, DisconnectUser = 21, ToHost = 32, ToMember = 33, ToAllMembers = 34, UdpBind = 35, LoginOk = 128, LoginFailed = 129, Pong = 130, ServerStarted = 144, LobbyList = 145, JoinResult = 146, LobbyClosed = 147, MemberLeft = 148, FromMember = 160, FromHost = 161, UdpBindAck = 163, Notice = 192, Disconnect = 193 } public static class Identity { public static ulong PlatformIdFromSecret(ReadOnlySpan<byte> secret) { Sha256 sha = new Sha256(); sha.Update(Encoding.ASCII.GetBytes("FWN player id v1")); sha.Update(secret); byte[] array = new byte[32]; sha.Final(array); ulong num = 0uL; for (int i = 0; i < 8; i++) { num |= (ulong)array[i] << 8 * i; } num &= 0x7FFFFFFFFFFFFFFFL; if (num != 0L) { return num; } return 1uL; } } public sealed class RelayClientOptions { public string Hostname = "sync.fischyweb.com"; public int TcpPort = 17777; public int UdpPort = 17778; public string ServerPublicKey = ""; public KnownServers KnownServers; public string Username = ""; public byte[] Secret; public int FusionVersionMajor; public int FusionVersionMinor; public string ClientVersion = "1.0.0"; public int ConnectionTimeoutSeconds = 15; public Action<string> Log = delegate { }; } public enum RelayEventKind { FromHost, FromMember, ServerStarted, LobbyList, JoinResult, LobbyClosed, MemberLeft, Notice, Disconnected } public sealed class LobbyEntry { public ulong LobbyId; public Dictionary<string, string> Metadata = new Dictionary<string, string>(); } public sealed class RelayEvent { public RelayEventKind Kind; public ulong Sender; public byte[] Data; public bool Reliable; public uint RequestId; public List<LobbyEntry> Lobbies; public ulong LobbyId; public bool Ok; public DisconnectReason Reason; public string Text; } public sealed class RelayConnectResult { public bool Success; public RelayClient Client; public string Error; public DisconnectReason Reason; public bool Retryable; public string ServerFingerprint; } public sealed class LobbyQuery { public readonly List<KeyValuePair<string, string>> Equal = new List<KeyValuePair<string, string>>(); public readonly List<KeyValuePair<string, string>> NotEqual = new List<KeyValuePair<string, string>>(); public int MaxResults = 100; public const int MaxFilters = 16; public const int MaxValueBytes = 256; public LobbyQuery WithEqual(string key, string value) { Equal.Add(new KeyValuePair<string, string>(key, value ?? "")); return this; } public LobbyQuery WithNotEqual(string key, string value) { NotEqual.Add(new KeyValuePair<string, string>(key, value ?? "")); return this; } public void Write(NetWriter w) { w.WriteByte((byte)Math.Min(Equal.Count, 16)); for (int i = 0; i < Equal.Count && i < 16; i++) { w.WriteString(Equal[i].Key, 128); w.WriteString(Equal[i].Value, 256); } w.WriteByte((byte)Math.Min(NotEqual.Count, 16)); for (int j = 0; j < NotEqual.Count && j < 16; j++) { w.WriteString(NotEqual[j].Key, 128); w.WriteString(NotEqual[j].Value, 256); } w.WriteUInt16((ushort)Math.Max(1, Math.Min(MaxResults, 500))); } public static LobbyQuery Read(ref NetReader r) { LobbyQuery lobbyQuery = new LobbyQuery(); int num = r.ReadByte(); if (num > 16) { throw new ProtocolException("too many lobby filters"); } for (int i = 0; i < num; i++) { lobbyQuery.WithEqual(r.ReadString(128), r.ReadString(256)); } int num2 = r.ReadByte(); if (num2 > 16) { throw new ProtocolException("too many lobby filters"); } for (int j = 0; j < num2; j++) { lobbyQuery.WithNotEqual(r.ReadString(128), r.ReadString(256)); } lobbyQuery.MaxResults = Math.Max(1, Math.Min((int)r.ReadUInt16(), 500)); return lobbyQuery; } public bool Matches(IReadOnlyDictionary<string, string> metadata) { foreach (KeyValuePair<string, string> item in Equal) { if (!metadata.TryGetValue(item.Key, out var value) || value != item.Value) { return false; } } foreach (KeyValuePair<string, string> item2 in NotEqual) { if (metadata.TryGetValue(item2.Key, out var value2) && value2 == item2.Value) { return false; } } return true; } } public sealed class RelayClient : IDisposable { private const double PingInterval = 2.0; private const double UdpPingInterval = 1.0; private readonly RelayClientOptions _options; private readonly Socket _tcp; private readonly NetworkStream _stream; private readonly TcpCipher _tcpSend; private readonly TcpCipher _tcpRecv; private readonly object _tcpLock = new object(); private readonly BlockingCollection<byte[]> _outbox = new BlockingCollection<byte[]>(new ConcurrentQueue<byte[]>()); private readonly ConcurrentQueue<RelayEvent> _events = new ConcurrentQueue<RelayEvent>(); private readonly Socket _udp; private readonly UdpCipher _udpCipher; private readonly object _udpLock = new object(); private readonly byte[] _udpSendBuffer = new byte[1200]; private readonly IPEndPoint[] _udpCandidates; private volatile IPEndPoint _udpTarget; private readonly uint _connectionId; private readonly Thread _writer; private readonly ManualResetEventSlim _readEnded = new ManualResetEventSlim(initialState: false); private volatile bool _closed; private int _disconnectRaised; private long _lastTcpReceiveMs; private double _lastPing; private double _lastUdpPing; private double _lastBind; private double _startedAt = -1.0; private uint _pingSeq; private int _requestSeq; public long BytesSent; public long BytesReceived; public ulong PlatformId { get; } public string Username { get; } public string ServerName { get; } public string Motd { get; } public IPAddress ServerAddress { get; } public bool UdpBound { get; private set; } public float RttMs { get; private set; } = -1f; public bool IsClosed => _closed; private RelayClient(RelayClientOptions o, Socket tcp, NetworkStream stream, SessionKeys keys, TcpCipher send, TcpCipher recv, IPAddress address, ulong platformId, string username, uint connectionId, string serverName, string motd, List<byte[]> lan) { _options = o; _tcp = tcp; _stream = stream; _tcpSend = send; _tcpRecv = recv; ServerAddress = address; PlatformId = platformId; Username = username; ServerName = serverName; Motd = motd; _connectionId = connectionId; _udpCipher = new UdpCipher(keys.UdpClientToServer, keys.UdpServerToClient); keys.Dispose(); _lastTcpReceiveMs = MonoClock.Milliseconds; _udpCandidates = UdpCandidates(address, (IPEndPoint)tcp.LocalEndPoint, lan, o.UdpPort); if (_udpCandidates.Length > 1) { o.Log("Trying UDP via " + string.Join(" and ", _udpCandidates.Select((IPEndPoint c) => c.ToString()))); } try { _udp = new Socket(address.AddressFamily, SocketType.Dgram, ProtocolType.Udp); _udp.Bind(new IPEndPoint((address.AddressFamily == AddressFamily.InterNetworkV6) ? IPAddress.IPv6Any : IPAddress.Any, 0)); _udp.ReceiveTimeout = 500; if (_udpCandidates.Length == 1) { _udpTarget = _udpCandidates[0]; } } catch (SocketException ex) { o.Log($"UDP unavailable ({ex.SocketErrorCode}); real-time data will use TCP"); _udp = null; } Thread thread = new Thread(TcpReadLoop); thread.IsBackground = true; thread.Name = "FWN-TCP-Read"; thread.Start(); _writer = new Thread(TcpWriteLoop) { IsBackground = true, Name = "FWN-TCP-Write" }; _writer.Start(); if (_udp != null) { Thread thread2 = new Thread(UdpReadLoop); thread2.IsBackground = true; thread2.Name = "FWN-UDP-Read"; thread2.Start(); } } public static async Task<RelayConnectResult> ConnectAsync(RelayClientOptions o, CancellationToken cancel) { if (o.Secret == null || o.Secret.Length != 32) { throw new ArgumentException("identity secret must be 32 bytes"); } string host = (o.Hostname ?? "").Trim(); int num = Math.Max(3, o.ConnectionTimeoutSeconds) * 1000; using CancellationTokenSource deadline = CancellationTokenSource.CreateLinkedTokenSource(cancel); deadline.CancelAfter(num); CancellationToken ct = deadline.Token; IPAddress[] source; try { if (IPAddress.TryParse(host, out IPAddress address)) { source = new IPAddress[1] { address }; } else { Task<IPAddress[]> dns = Dns.GetHostAddressesAsync(host); if (await Task.WhenAny(dns, Task.Delay(Math.Min(num, 8000), ct)).ConfigureAwait(continueOnCapturedContext: false) != dns) { return Fail(DisconnectReason.Timeout, "DNS lookup for " + host + " timed out", retryable: true); } source = dns.Result; } } catch (Exception e) { return Fail(DisconnectReason.None, $"Could not resolve {host} ({Inner(e).Message})", retryable: true); } source = source.OrderBy((IPAddress a) => (a.AddressFamily != AddressFamily.InterNetwork) ? 1 : 0).ToArray(); if (source.Length == 0) { return Fail(DisconnectReason.None, host + " has no addresses", retryable: true); } string msg = "unreachable"; IPAddress[] array = source; foreach (IPAddress address2 in array) { if (ct.IsCancellationRequested) { break; } RelayConnectResult relayConnectResult = await TryAddressAsync(o, host, address2, ct, cancel).ConfigureAwait(continueOnCapturedContext: false); if (relayConnectResult.Success || !relayConnectResult.Retryable || relayConnectResult.Reason != DisconnectReason.None) { return relayConnectResult; } msg = relayConnectResult.Error; } return Fail(DisconnectReason.None, msg, retryable: true); } private static RelayConnectResult Fail(DisconnectReason r, string msg, bool retryable) { return new RelayConnectResult { Success = false, Reason = r, Error = msg, Retryable = retryable }; } private static Exception Inner(Exception e) { if (!(e is AggregateException { InnerException: not null } ex)) { return e; } return ex.InnerException; } private static async Task<RelayConnectResult> TryAddressAsync(RelayClientOptions o, string host, IPAddress address, CancellationToken ct, CancellationToken userCancel) { Socket socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; bool keep = false; using (ct.Register(delegate { if (!keep) { try { socket.Dispose(); } catch (Exception) { } } })) { _ = 4; try { try { await socket.ConnectAsync(new IPEndPoint(address, o.TcpPort)).ConfigureAwait(continueOnCapturedContext: false); } catch (Exception e) { if (ct.IsCancellationRequested) { throw new OperationCanceledException(); } SocketException ex = Inner(e) as SocketException; string value = ((ex != null && ex.SocketErrorCode == SocketError.ConnectionRefused) ? "connection refused - is the server running?" : (ex?.SocketErrorCode.ToString() ?? Inner(e).Message)); return Fail(DisconnectReason.None, $"Cannot reach {host}:{o.TcpPort}: {value}", retryable: true); } socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, optionValue: true); NetworkStream stream = new NetworkStream(socket, ownsSocket: true); byte[] eph = X25519.GeneratePrivateKey(); byte[] hello = Handshake.BuildClientHello(X25519.PublicKeyFromPrivate(eph)); byte[] array = FrameIO.PlainFrame(hello); await stream.WriteAsync(array, 0, array.Length, ct).ConfigureAwait(continueOnCapturedContext: false); byte[] item = (await FrameIO.ReadFrameAsync(stream, 512, ct).ConfigureAwait(continueOnCapturedContext: false)).Item2; if (item[0] == 3) { Handshake.ParseReject(item, out var _, out var reason, out var text); stream.Dispose(); return Fail(reason, string.IsNullOrEmpty(text) ? DisconnectReasonText.Describe(reason) : text, DisconnectReasonText.IsRetryable(reason)); } Handshake.ParseServerHello(item, out var sStatic, out var serverEphemeralPublic, out var confirmTag); string text2 = CryptoUtil.ToHex(sStatic); string text3 = (string.IsNullOrEmpty(o.ServerPublicKey) ? null : o.ServerPublicKey.Trim().ToLowerInvariant()); string text4 = o.KnownServers?.Get(host, o.TcpPort); if (text3 != null && text3 != text2) { stream.Dispose(); return Fail(DisconnectReason.HandshakeFailed, "Server identity does not match the pinned key", retryable: false); } if (text3 == null && text4 != null && text4 != text2) { stream.Dispose(); return Fail(DisconnectReason.HandshakeFailed, "Server identity changed since your last visit (delete known_servers.json if the server was reinstalled)", retryable: false); } SessionKeys keys = Handshake.ClientFinish(hello, eph, sStatic, serverEphemeralPublic, confirmTag); CryptoUtil.Zero(eph); if (text3 == null && text4 == null) { o.KnownServers?.Remember(host, o.TcpPort, text2); } TcpCipher send = new TcpCipher(keys.TcpClientToServer); TcpCipher recv = new TcpCipher(keys.TcpServerToClient); NetWriter netWriter = new NetWriter(160); netWriter.WriteByte(1); netWriter.WriteUInt16((ushort)o.FusionVersionMajor); netWriter.WriteUInt16((ushort)o.FusionVersionMinor); netWriter.WriteShortString(o.ClientVersion, 32); netWriter.WriteShortString(o.Username, 96); netWriter.WriteBytes(o.Secret); byte[] array2 = send.Seal(netWriter.Span); await stream.WriteAsync(array2, 0, array2.Length, ct).ConfigureAwait(continueOnCapturedContext: false); if (!ParseLogin(await ReadRecordAsync(stream, recv, ct).ConfigureAwait(continueOnCapturedContext: false), out var id, out var name, out var cid, out var serverName, out var motd, out var lan, out var reason2, out var text5)) { stream.Dispose(); return Fail(reason2, text5, DisconnectReasonText.IsRetryable(reason2)); } keep = true; RelayClient client = new RelayClient(o, socket, stream, keys, send, recv, address, id, name, cid, serverName, motd, lan); return new RelayConnectResult { Success = true, Client = client, ServerFingerprint = CryptoUtil.Fingerprint(sStatic) }; } catch (OperationCanceledException) { bool isCancellationRequested = userCancel.IsCancellationRequested; return Fail(isCancellationRequested ? DisconnectReason.ClientLeft : DisconnectReason.Timeout, isCancellationRequested ? "Cancelled" : $"Timed out connecting to {host}:{o.TcpPort}", !isCancellationRequested); } catch (ProtocolException ex3) { return Fail(DisconnectReason.HandshakeFailed, "Handshake failed: " + ex3.Message, retryable: false); } catch (Exception ex4) when (ex4 is IOException || ex4 is SocketException || ex4 is ObjectDisposedException) { if (ct.IsCancellationRequested) { return Fail(DisconnectReason.Timeout, $"Timed out connecting to {host}:{o.TcpPort}", retryable: true); } return Fail(DisconnectReason.None, $"Connection to {host}:{o.TcpPort} closed during login ({Inner(ex4).Message})", retryable: true); } finally { if (!keep) { try { socket.Dispose(); } catch (Exception) { } } } } } private static async Task<byte[]> ReadRecordAsync(NetworkStream stream, TcpCipher recv, CancellationToken ct) { (byte[], byte[]) obj = await FrameIO.ReadFrameAsync(stream, 1048832, ct).ConfigureAwait(continueOnCapturedContext: false); byte[] item = obj.Item1; byte[] item2 = obj.Item2; byte[] array = recv.Open(item, item2); if (array == null || array.Length == 0) { throw new ProtocolException("relay frame failed authentication"); } return array; } private static bool ParseLogin(byte[] m, out ulong id, out string name, out uint cid, out string serverName, out string motd, out List<byte[]> lan, out DisconnectReason reason, out string text) { id = 0uL; name = ""; cid = 0u; serverName = ""; motd = ""; lan = new List<byte[]>(); reason = DisconnectReason.None; text = ""; NetReader netReader = new NetReader(new ReadOnlySpan<byte>(m, 1, m.Length - 1)); RelayMessage relayMessage = (RelayMessage)m[0]; switch (relayMessage) { case RelayMessage.LoginFailed: case RelayMessage.Disconnect: reason = (DisconnectReason)netReader.ReadByte(); text = netReader.ReadString(1024); if (string.IsNullOrEmpty(text)) { text = DisconnectReasonText.Describe(reason); } return false; default: throw new ProtocolException($"unexpected {relayMessage} during login"); case RelayMessage.LoginOk: { id = netReader.ReadUInt64(); name = netReader.ReadShortString(96); cid = netReader.ReadUInt32(); serverName = netReader.ReadShortString(128); motd = netReader.ReadString(1024); int num = netReader.ReadCount(8, 4); for (int i = 0; i < num; i++) { lan.Add(netReader.ReadBytes(4).ToArray()); } return true; } } } public static IPEndPoint[] UdpCandidates(IPAddress server, IPEndPoint local, List<byte[]> lan, int udpPort) { List<IPEndPoint> list = new List<IPEndPoint> { new IPEndPoint(server, udpPort) }; if (local == null || lan == null || server.AddressFamily != AddressFamily.InterNetwork) { return list.ToArray(); } byte[] addressBytes = local.Address.MapToIPv4().GetAddressBytes(); if (addressBytes[0] != 10 && (addressBytes[0] != 172 || addressBytes[1] < 16 || addressBytes[1] > 31) && (addressBytes[0] != 192 || addressBytes[1] != 168) && addressBytes[0] != 127) { return list.ToArray(); } foreach (byte[] item in lan) { if (item != null && item.Length == 4) { IPAddress iPAddress = new IPAddress(item); if (!iPAddress.Equals(server)) { list.Add(new IPEndPoint(iPAddress, udpPort)); } } } return list.ToArray(); } private void TcpReadLoop() { DisconnectReason disconnectReason = DisconnectReason.Timeout; string text = "Lost connection to the Fischyweb server"; try { byte[] array = new byte[4]; while (!_closed) { ReadExactly(array); int num = array[0] | (array[1] << 8) | (array[2] << 16) | (array[3] << 24); if (num <= 0 || num > 1048832) { throw new ProtocolException("bad frame length"); } byte[] array2 = new byte[num]; ReadExactly(array2); Interlocked.Add(ref BytesReceived, num + 4); byte[] array3 = _tcpRecv.Open(array, array2); if (array3 == null || array3.Length == 0) { throw new ProtocolException("frame failed authentication"); } _lastTcpReceiveMs = MonoClock.Milliseconds; if (array3[0] == 193) { NetReader netReader = new NetReader(new ReadOnlySpan<byte>(array3, 1, array3.Length - 1)); disconnectReason = (DisconnectReason)netReader.ReadByte(); text = netReader.ReadString(1024); if (string.IsNullOrEmpty(text)) { text = DisconnectReasonText.Describe(disconnectReason); } break; } Dispatch(array3, reliable: true); } } catch (ProtocolException ex) { disconnectReason = DisconnectReason.ProtocolError; text = "Protocol error: " + ex.Message; } catch (Exception) { if (_closed) { _readEnded.Set(); return; } } _readEnded.Set(); RaiseDisconnected(disconnectReason, text); } private void ReadExactly(byte[] buffer) { int num; for (int i = 0; i < buffer.Length; i += num) { num = _stream.Read(buffer, i, buffer.Length - i); if (num <= 0) { throw new EndOfStreamException(); } } } private void TcpWriteLoop() { try { foreach (byte[] item in _outbox.GetConsumingEnumerable()) { _stream.Write(item, 0, item.Length); Interlocked.Add(ref BytesSent, item.Length); } } catch (Exception) { if (!_closed) { RaiseDisconnected(DisconnectReason.Timeout, "Lost connection to the Fischyweb server"); } } } private bool IsCandidate(IPEndPoint ep) { IPAddress obj = (ep.Address.IsIPv4MappedToIPv6 ? ep.Address.MapToIPv4() : ep.Address); IPEndPoint[] udpCandidates = _udpCandidates; foreach (IPEndPoint iPEndPoint in udpCandidates) { if (iPEndPoint.Address.Equals(obj) && iPEndPoint.Port == ep.Port) { return true; } } return false; } private void UdpReadLoop() { byte[] array = new byte[2048]; byte[] array2 = new byte[1171]; EndPoint endPoint = new IPEndPoint((_udp.AddressFamily == AddressFamily.InterNetworkV6) ? IPAddress.IPv6Any : IPAddress.Any, 0); while (!_closed) { EndPoint remoteEP = endPoint; int num; try { num = _udp.ReceiveFrom(array, ref remoteEP); } catch (SocketException) { continue; } catch (ObjectDisposedException) { break; } IPEndPoint iPEndPoint = (IPEndPoint)remoteEP; if (num <= 0 || !IsCandidate(iPEndPoint)) { continue; } ReadOnlySpan<byte> datagram = new ReadOnlySpan<byte>(array, 0, num); if (!UdpCipher.TryPeekConnectionId(datagram, out var connectionId) || connectionId != _connectionId) { continue; } ulong packetNumber; int num2 = _udpCipher.Open(datagram, array2, out packetNumber); if (num2 <= 0) { continue; } if (_udpTarget == null) { _udpTarget = iPEndPoint; _options.Log($"UDP connected via {iPEndPoint}"); } UdpBound = true; Interlocked.Add(ref BytesReceived, num); byte[] array3 = array2.AsSpan(0, num2).ToArray(); if (array3[0] == 163) { continue; } if (array3[0] == 130) { HandlePong(array3); continue; } try { Dispatch(array3, reliable: false); } catch (ProtocolException) { } } } private void HandlePong(byte[] m) { NetReader netReader = new NetReader(new ReadOnlySpan<byte>(m, 1, m.Length - 1)); netReader.ReadUInt32(); uint b = netReader.ReadUInt32(); RttMs = MonoClock.Diff(MonoClock.WireMs, b); } private void Dispatch(byte[] m, bool reliable) { RelayMessage relayMessage = (RelayMessage)m[0]; NetReader netReader = new NetReader(new ReadOnlySpan<byte>(m, 1, m.Length - 1)); switch (relayMessage) { case RelayMessage.Pong: HandlePong(m); break; case RelayMessage.FromMember: case RelayMessage.FromHost: { ulong sender = netReader.ReadUInt64(); byte[] data = netReader.ReadBlob(1048576).ToArray(); _events.Enqueue(new RelayEvent { Kind = ((relayMessage != RelayMessage.FromHost) ? RelayEventKind.FromMember : RelayEventKind.FromHost), Sender = sender, Data = data, Reliable = reliable }); break; } case RelayMessage.ServerStarted: _events.Enqueue(new RelayEvent { Kind = RelayEventKind.ServerStarted, LobbyId = netReader.ReadUInt64() }); break; case RelayMessage.LobbyList: { RelayEvent relayEvent2 = new RelayEvent { Kind = RelayEventKind.LobbyList, RequestId = netReader.ReadUInt32(), Lobbies = new List<LobbyEntry>() }; int num = netReader.ReadCount16(1000, 10); for (int i = 0; i < num; i++) { LobbyEntry lobbyEntry = new LobbyEntry { LobbyId = netReader.ReadUInt64() }; int num2 = netReader.ReadCount16(64, 4); for (int j = 0; j < num2; j++) { lobbyEntry.Metadata[netReader.ReadString(128)] = netReader.ReadString(61440); } relayEvent2.Lobbies.Add(lobbyEntry); } _events.Enqueue(relayEvent2); break; } case RelayMessage.JoinResult: { RelayEvent relayEvent = new RelayEvent { Kind = RelayEventKind.JoinResult, LobbyId = netReader.ReadUInt64(), Ok = netReader.ReadBool() }; relayEvent.Reason = (DisconnectReason)netReader.ReadByte(); relayEvent.Text = netReader.ReadString(1024); _events.Enqueue(relayEvent); break; } case RelayMessage.LobbyClosed: { ulong lobbyId = netReader.ReadUInt64(); DisconnectReason disconnectReason = (DisconnectReason)netReader.ReadByte(); string text = netReader.ReadString(1024); _events.Enqueue(new RelayEvent { Kind = RelayEventKind.LobbyClosed, LobbyId = lobbyId, Reason = disconnectReason, Text = (string.IsNullOrEmpty(text) ? DisconnectReasonText.Describe(disconnectReason) : text) }); break; } case RelayMessage.MemberLeft: _events.Enqueue(new RelayEvent { Kind = RelayEventKind.MemberLeft, Sender = netReader.ReadUInt64() }); break; case RelayMessage.Notice: netReader.ReadByte(); _events.Enqueue(new RelayEvent { Kind = RelayEventKind.Notice, Text = netReader.ReadString(1024) }); break; } } private void RaiseDisconnected(DisconnectReason reason, string text) { if (Interlocked.Exchange(ref _disconnectRaised, 1) == 0) { _events.Enqueue(new RelayEvent { Kind = RelayEventKind.Disconnected, Reason = reason, Text = text }); Close(); } } public bool TryGetEvent(out RelayEvent e) { return _events.TryDequeue(out e); } public void Update(double now) { if (_closed) { return; } if (_startedAt < 0.0) { _startedAt = now; } if (now - _lastPing >= 2.0) { _lastPing = now; NetWriter netWriter = new NetWriter(16); netWriter.WriteByte(2); netWriter.WriteUInt32(++_pingSeq); netWriter.WriteUInt32(MonoClock.WireMs); SendTcp(netWriter.Span); } if (MonoClock.Milliseconds - _lastTcpReceiveMs > (long)Math.Max(5, _options.ConnectionTimeoutSeconds) * 1000L) { RaiseDisconnected(DisconnectReason.Timeout, "The Fischyweb server stopped responding"); } else { if (_udp == null) { return; } if (!UdpBound) { double num = ((now - _startedAt < 3.0) ? 0.25 : 1.0); if (now - _lastBind >= num) { _lastBind = now; SendUdp("#"u8); } } else if (now - _lastUdpPing >= 1.0) { _lastUdpPing = now; NetWriter netWriter2 = new NetWriter(16); netWriter2.WriteByte(2); netWriter2.WriteUInt32(++_pingSeq); netWriter2.WriteUInt32(MonoClock.WireMs); SendUdp(netWriter2.Span); } } } private void SendTcp(ReadOnlySpan<byte> plaintext) { if (_closed) { return; } lock (_tcpLock) { byte[] item = _tcpSend.Seal(plaintext); try { _outbox.Add(item); } catch (InvalidOperationException) { } } } private void SendUdp(ReadOnlySpan<byte> plaintext) { if (_udp == null || _closed) { return; } lock (_udpLock) { try { int num = _udpCipher.Seal(_connectionId, plaintext, _udpSendBuffer); IPEndPoint udpTarget = _udpTarget; if (udpTarget != null) { _udp.SendTo(_udpSendBuffer, 0, num, SocketFlags.None, udpTarget); Interlocked.Add(ref BytesSent, num); return; } IPEndPoint[] udpCandidates = _udpCandidates; foreach (IPEndPoint remoteEP in udpCandidates) { try { _udp.SendTo(_udpSendBuffer, 0, num, SocketFlags.None, remoteEP); } catch (SocketException) { } Interlocked.Add(ref BytesSent, num); } } catch (SocketException) { } catch (ObjectDisposedException) { } } } private void SendData(RelayMessage type, ulong? target, ReadOnlySpan<byte> data, bool reliable) { if (data.Length > 1048576) { throw new ArgumentException("message too large"); } NetWriter netWriter = new NetWriter(data.Length + 16); netWriter.WriteByte((byte)type); if (target.HasValue) { netWriter.WriteUInt64(target.Value); } netWriter.WriteBlob(data); if (!reliable && UdpBound && data.Length <= 1158) { SendUdp(netWriter.Span); } else { SendTcp(netWriter.Span); } } public void SendToHost(ReadOnlySpan<byte> data, bool reliable) { SendData(RelayMessage.ToHost, null, data, reliable); } public void SendToMember(ulong member, ReadOnlySpan<byte> data, bool reliable) { SendData(RelayMessage.ToMember, member, data, reliable); } public void SendToAllMembers(ReadOnlySpan<byte> data, bool reliable) { SendData(RelayMessage.ToAllMembers, null, data, reliable); } public void StartServer() { SendTcp(new byte[1] { 16 }); } public void Leave() { SendTcp(new byte[1] { 20 }); } public void JoinServer(ulong lobbyId) { NetWriter netWriter = new NetWriter(16); netWriter.WriteByte(19); netWriter.WriteUInt64(lobbyId); SendTcp(netWriter.Span); } public void DisconnectUser(ulong platformId) { NetWriter netWriter = new NetWriter(16); netWriter.WriteByte(21); netWriter.WriteUInt64(platformId); SendTcp(netWriter.Span); } public void SetLobbyMetadata(string key, string value) { NetWriter netWriter = new NetWriter(64 + (value?.Length ?? 0) * 2); netWriter.WriteByte(17); netWriter.WriteString(key ?? "", 128); netWriter.WriteString(value ?? "", 61440); SendTcp(netWriter.Span); } public uint RequestLobbies(LobbyQuery q) { uint num = (uint)Interlocked.Increment(ref _requestSeq); NetWriter netWriter = new NetWriter(); netWriter.WriteByte(18); netWriter.WriteUInt32(num); q.Write(netWriter); SendTcp(netWriter.Span); return num; } public void Disconnect() { if (!_closed) { Interlocked.Exchange(ref _disconnectRaised, 1); SendTcp(new byte[1] { 20 }); try { _outbox.CompleteAdding(); } catch (Exception) { } _writer?.Join(500); try { _tcp.Shutdown(SocketShutdown.Send); } catch (Exception) { } _readEnded.Wait(300); Close(); } } private void Close() { if (_closed) { return; } _closed = true; try { _outbox.CompleteAdding(); } catch (Exception) { } try { _stream.Dispose(); } catch (Exception) { } try { _tcp.Dispose(); } catch (Exception) { } try { _udp?.Dispose(); } catch (Exception) { } } public void Dispose() { Close(); } } public sealed class KnownServers { private readonly string _path; private readonly Dictionary<string, string> _keys = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private readonly object _lock = new object(); public KnownServers(string path) { _path = path; if (path == null || !File.Exists(path)) { return; } try { foreach (KeyValuePair<string, object> item in MiniJson.ParseObject(File.ReadAllText(path))) { if (item.Value is string { Length: 64 } text) { _keys[item.Key] = text.ToLowerInvariant(); } } } catch (Exception) { } } private static string KeyFor(string host, int port) { return $"{host.Trim().ToLowerInvariant()}:{port}"; } public string Get(string host, int port) { lock (_lock) { string value; return _keys.TryGetValue(KeyFor(host, port), out value) ? value : null; } } public void Remember(string host, int port, string keyHex) { lock (_lock) { _keys[KeyFor(host, port)] = keyHex.ToLowerInvariant(); if (_path == null) { return; } try { Dictionary<string, object> dictionary = new Dictionary<string, object>(); foreach (KeyValuePair<string, string> key in _keys) { dictionary[key.Key] = key.Value; } Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(_path)) ?? "."); File.WriteAllText(_path, MiniJson.Serialize(dictionary)); } catch (Exception) { } } } } public ref struct NetReader { private readonly ReadOnlySpan<byte> _data; private int _pos; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); public int Position => _pos; public int Remaining => _data.Length - _pos; public bool AtEnd => _pos >= _data.Length; public NetReader(ReadOnlySpan<byte> data) { _data = data; _pos = 0; } private void Need(int n) { if (n < 0 || _pos + n > _data.Length) { throw new ProtocolException("message truncated"); } } public byte ReadByte() { Need(1); return _data[_pos++]; } public bool ReadBool() { byte num = ReadByte(); if (num > 1) { throw new ProtocolException("invalid bool"); } return num == 1; } public ushort ReadUInt16() { Need(2); ushort result = (ushort)(_data[_pos] | (_data[_pos + 1] << 8)); _pos += 2; return result; } public short ReadInt16() { return (short)ReadUInt16(); } public uint ReadUInt32() { Need(4); int result = _data[_pos] | (_data[_pos + 1] << 8) | (_data[_pos + 2] << 16) | (_data[_pos + 3] << 24); _pos += 4; return (uint)result; } public int ReadInt32() { return (int)ReadUInt32(); } public ulong ReadUInt64() { long num = ReadUInt32(); ulong num2 = ReadUInt32(); return (ulong)num | (num2 << 32); } public float ReadSingle() { float num = BitConverter.Int32BitsToSingle((int)ReadUInt32()); if (float.IsNaN(num) || float.IsInfinity(num)) { throw new ProtocolException("non-finite float"); } return num; } public float ReadSingleBounded(float limit) { float num = ReadSingle(); if (num > limit || num < 0f - limit) { throw new ProtocolException("float out of range"); } return num; } public ReadOnlySpan<byte> ReadBytes(int count) { Need(count); ReadOnlySpan<byte> result = _data.Slice(_pos, count); _pos += count; return result; } public ReadOnlySpan<byte> ReadBlob(int maxBytes) { uint num = ReadUInt32(); if (num > (uint)maxBytes) { throw new ProtocolException("blob too large"); } return ReadBytes((int)num); } public string ReadShortString(int maxBytes = 255) { int num = ReadByte(); if (num > maxBytes) { throw new ProtocolException("string too long"); } return Decode(ReadBytes(num)); } public string ReadString(int maxBytes = 4096) { int num = ReadUInt16(); if (num > maxBytes) { throw new ProtocolException("string too long"); } return Decode(ReadBytes(num)); } private static string Decode(ReadOnlySpan<byte> bytes) { try { return StrictUtf8.GetString(bytes); } catch (ArgumentException) { throw new ProtocolException("invalid UTF-8"); } } public int ReadCount(int max, int minBytesPerItem) { byte num = ReadByte(); if (num > max) { throw new ProtocolException("count too large"); } if (num * Math.Max(1, minBytesPerItem) > Remaining) { throw new ProtocolException("count exceeds payload"); } return num; } public int ReadCount16(int max, int minBytesPerItem) { ushort num = ReadUInt16(); if (num > max) { throw new ProtocolException("count too large"); } if ((long)(int)num * (long)Math.Max(1, minBytesPerItem) > Remaining) { throw new ProtocolException("count exceeds payload"); } return num; } public void ExpectEnd() { if (!AtEnd) { throw new ProtocolException("trailing bytes"); } } } public sealed class ProtocolException : Exception { public ProtocolException(string message) : base(message) { } } public sealed class NetWriter { private byte[] _buffer; private int _length; public int Length => _length; public ReadOnlySpan<byte> Span => new ReadOnlySpan<byte>(_buffer, 0, _length); public ArraySegment<byte> Segment => new ArraySegment<byte>(_buffer, 0, _length); public NetWriter(int capacity = 256) { _buffer = new byte[Math.Max(16, capacity)]; } public byte[] ToArray() { return Span.ToArray(); } public void Reset() { _length = 0; } private void Ensure(int extra) { if (_length + extra > _buffer.Length) { int num; for (num = _buffer.Length * 2; num < _length + extra; num *= 2) { } Array.Resize(ref _buffer, num); } } public int Reserve(int count) { Ensure(count); int length = _length; _length += count; return length; } public void PatchUInt16(int offset, ushort value) { _buffer[offset] = (byte)value; _buffer[offset + 1] = (byte)(value >> 8); } public void WriteByte(byte v) { Ensure(1); _buffer[_length++] = v; } public void WriteBool(bool v) { WriteByte(v ? ((byte)1) : ((byte)0)); } public void WriteSByte(sbyte v) { WriteByte((byte)v); } public void WriteUInt16(ushort v) { Ensure(2); _buffer[_length++] = (byte)v; _buffer[_length++] = (byte)(v >> 8); } public void WriteInt16(short v) { WriteUInt16((ushort)v); } public void WriteUInt32(uint v) { Ensure(4); _buffer[_length++] = (byte)v; _buffer[_length++] = (byte)(v >> 8); _buffer[_length++] = (byte)(v >> 16); _buffer[_length++] = (byte)(v >> 24); } public void WriteInt32(int v) { WriteUInt32((uint)v); } public void WriteUInt64(ulong v) { WriteUInt32((uint)v); WriteUInt32((uint)(v >> 32)); } public void WriteInt64(long v) { WriteUInt64((ulong)v); } public void WriteSingle(float v) { WriteUInt32((uint)BitConverter.SingleToInt32Bits(v)); } public void WriteBytes(ReadOnlySpan<byte> data) { Ensure(data.Length); data.CopyTo(new Span<byte>(_buffer, _length, data.Length)); _length += data.Length; } public void WriteBlob(ReadOnlySpan<byte> data) { WriteUInt32((uint)data.Length); WriteBytes(data); } public void WriteShortString(string s, int maxBytes = 255) { byte[] array = Utf8Truncate(s ?? string.Empty, Math.Min(maxBytes, 255)); WriteByte((byte)array.Length); WriteBytes(array); } public void WriteString(string s, int maxBytes = 4096) { byte[] array = Utf8Truncate(s ?? string.Empty, Math.Min(maxBytes, 65535)); WriteUInt16((ushort)array.Length); WriteBytes(array); } internal static byte[] Utf8Truncate(string s, int maxBytes) { byte[] bytes = Encoding.UTF8.GetBytes(s); if (bytes.Length <= maxBytes) { return bytes; } int num = maxBytes; while (num > 0 && (bytes[num] & 0xC0) == 128) { num--; } byte[] array = new byte[num]; Array.Copy(bytes, array, num); return array; } internal static short Quantize(float value, float unit) { if (float.IsNaN(value)) { return 0; } float num = value / unit; if (num > 32767f) { return short.MaxValue; } if (num < -32768f) { return short.MinValue; } return (short)Math.Round(num); } } } namespace FischywebNetworker.Shared.Crypto { public sealed class ChaCha20Poly1305 { public const int KeySize = 32; public const int NonceSize = 12; public const int TagSize = 16; private readonly uint[] _key = new uint[8]; public ChaCha20Poly1305(ReadOnlySpan<byte> key) { if (key.Length != 32) { throw new ArgumentException("ChaCha20-Poly1305 key must be 32 bytes"); } for (int i = 0; i < 8; i++) { _key[i] = LoadLE32(key, 4 * i); } } public void Encrypt(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> plaintext, Span<byte> ciphertext, Span<byte> tag, ReadOnlySpan<byte> associatedData = default(ReadOnlySpan<byte>)) { if (nonce.Length != 12) { throw new ArgumentException("nonce must be 12 bytes"); } if (ciphertext.Length < plaintext.Length) { throw new ArgumentException("ciphertext too small"); } Span<byte> output = stackalloc byte[64]; Block(nonce, 0u, output); Xor(nonce, 1u, plaintext, ciphertext); ComputeTag(output.Slice(0, 32), associatedData, ciphertext.Slice(0, plaintext.Length), tag); output.Clear(); } public bool Decrypt(ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> tag, Span<byte> plaintext, ReadOnlySpan<byte> associatedData = default(ReadOnlySpan<byte>)) { if (nonce.Length != 12 || tag.Length != 16) { return false; } if (plaintext.Length < ciphertext.Length) { throw new ArgumentException("plaintext too small"); } Span<byte> output = stackalloc byte[64]; Block(nonce, 0u, output); Span<byte> span = stackalloc byte[16]; ComputeTag(output.Slice(0, 32), associatedData, ciphertext, span); output.Clear(); if (!CryptoUtil.FixedTimeEquals(span, tag)) { return false; } Xor(nonce, 1u, ciphertext, plaintext); return true; } private static void ComputeTag(ReadOnlySpan<byte> polyKey, ReadOnlySpan<byte> ad, ReadOnlySpan<byte> ct, Span<byte> tag) { Poly1305 poly = new Poly1305(polyKey); Span<byte> span = stackalloc byte[16]; span.Clear(); poly.Update(ad); if ((ad.Length & 0xF) != 0) { poly.Update(span.Slice(0, 16 - (ad.Length & 0xF))); } poly.Update(ct); if ((ct.Length & 0xF) != 0) { poly.Update(span.Slice(0, 16 - (ct.Length & 0xF))); } Span<byte> span2 = stackalloc byte[16]; StoreLE64(span2, 0, (ulong)ad.Length); StoreLE64(span2, 8, (ulong)ct.Length); poly.Update(span2); poly.Final(tag); } private void Xor(ReadOnlySpan<byte> nonce, uint counter, ReadOnlySpan<byte> input, Span<byte> output) { Span<byte> output2 = stackalloc byte[64]; int num; for (int i = 0; i < input.Length; i += num) { Block(nonce, counter++, output2); num = Math.Min(64, input.Length - i); for (int j = 0; j < num; j++) { output[i + j] = (byte)(input[i + j] ^ output2[j]); } } output2.Clear(); } public void Block(ReadOnlySpan<byte> nonce, uint counter, Span<byte> output) { Span<uint> span = stackalloc uint[16]; span[0] = 1634760805u; span[1] = 857760878u; span[2] = 2036477234u; span[3] = 1797285236u; for (int i = 0; i < 8; i++) { span[4 + i] = _key[i]; } span[12] = counter; span[13] = LoadLE32(nonce, 0); span[14] = LoadLE32(nonce, 4); span[15] = LoadLE32(nonce, 8); Span<uint> span2 = stackalloc uint[16]; span.CopyTo(span2); for (int j = 0; j < 10; j++) { Quarter(span2, 0, 4, 8, 12); Quarter(span2, 1, 5, 9, 13); Quarter(span2, 2, 6, 10, 14); Quarter(span2, 3, 7, 11, 15); Quarter(span2, 0, 5, 10, 15); Quarter(span2, 1, 6, 11, 12); Quarter(span2, 2, 7, 8, 13); Quarter(span2, 3, 4, 9, 14); } for (int k = 0; k < 16; k++) { StoreLE32(output, 4 * k, span2[k] + span[k]); } span2.Clear(); span.Clear(); } private static uint Rotl(uint v, int c) { return (v << c) | (v >> 32 - c); } private static void Quarter(Span<uint> x, int a, int b, int c, int d) { x[a] += x[b]; x[d] = Rotl(x[d] ^ x[a], 16); x[c] += x[d]; x[b] = Rotl(x[b] ^ x[c], 12); x[a] += x[b]; x[d] = Rotl(x[d] ^ x[a], 8); x[c] += x[d]; x[b] = Rotl(x[b] ^ x[c], 7); } internal static uint LoadLE32(ReadOnlySpan<byte> b, int o) { return (uint)(b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)); } internal static void StoreLE32(Span<byte> b, int o, uint v) { b[o] = (byte)v; b[o + 1] = (byte)(v >> 8); b[o + 2] = (byte)(v >> 16); b[o + 3] = (byte)(v >> 24); } internal static void StoreLE64(Span<byte> b, int o, ulong v) { for (int i = 0; i < 8; i++) { b[o + i] = (byte)(v >> 8 * i); } } } public sealed class Poly1305 { private readonly uint _r0; private readonly uint _r1; private readonly uint _r2; private readonly uint _r3; private readonly uint _r4; private readonly uint _s1; private readonly uint _s2; private readonly uint _s3; private readonly uint _s4; private readonly uint _pad0; private readonly uint _pad1; private readonly uint _pad2; private readonly uint _pad3; private uint _h0; private uint _h1; private uint _h2; private uint _h3; private uint _h4; private readonly byte[] _buffer = new byte[16]; private int _leftover; public Poly1305(ReadOnlySpan<byte> key) { if (key.Length < 32) { throw new ArgumentException("Poly1305 key must be 32 bytes"); } _r0 = ChaCha20Poly1305.LoadLE32(key, 0) & 0x3FFFFFF; _r1 = (ChaCha20Poly1305.LoadLE32(key, 3) >> 2) & 0x3FFFF03; _r2 = (ChaCha20Poly1305.LoadLE32(key, 6) >> 4) & 0x3FFC0FF; _r3 = (ChaCha20Poly1305.LoadLE32(key, 9) >> 6) & 0x3F03FFF; _r4 = (ChaCha20Poly1305.LoadLE32(key, 12) >> 8) & 0xFFFFF; _s1 = _r1 * 5; _s2 = _r2 * 5; _s3 = _r3 * 5; _s4 = _r4 * 5; _pad0 = ChaCha20Poly1305.LoadLE32(key, 16); _pad1 = ChaCha20Poly1305.LoadLE32(key, 20); _pad2 = ChaCha20Poly1305.LoadLE32(key, 24); _pad3 = ChaCha20Poly1305.LoadLE32(key, 28); } public void Update(ReadOnlySpan<byte> m) { if (_leftover > 0) { int num = Math.Min(16 - _leftover, m.Length); m.Slice(0, num).CopyTo(_buffer.AsSpan(_leftover)); _leftover += num; m = m.Slice(num); if (_leftover < 16) { return; } Blocks(_buffer, 16777216u); _leftover = 0; } while (m.Length >= 16) { Blocks(m.Slice(0, 16), 16777216u); m = m.Slice(16); } if (m.Length > 0) { m.CopyTo(_buffer); _leftover = m.Length; } } private void Blocks(ReadOnlySpan<byte> m, uint hibit) { uint h = _h0; uint h2 = _h1; uint h3 = _h2; uint h4 = _h3; uint h5 = _h4; h += ChaCha20Poly1305.LoadLE32(m, 0) & 0x3FFFFFF; h2 += (ChaCha20Poly1305.LoadLE32(m, 3) >> 2) & 0x3FFFFFF; h3 += (ChaCha20Poly1305.LoadLE32(m, 6) >> 4) & 0x3FFFFFF; h4 += (ChaCha20Poly1305.LoadLE32(m, 9) >> 6) & 0x3FFFFFF; h5 += (ChaCha20Poly1305.LoadLE32(m, 12) >> 8) | hibit; ulong num = (ulong)((long)h * (long)_r0 + (long)h2 * (long)_s4 + (long)h3 * (long)_s3 + (long)h4 * (long)_s2 + (long)h5 * (long)_s1); ulong num2 = (ulong)((long)h * (long)_r1 + (long)h2 * (long)_r0 + (long)h3 * (long)_s4 + (long)h4 * (long)_s3 + (long)h5 * (long)_s2); ulong num3 = (ulong)((long)h * (long)_r2 + (long)h2 * (long)_r1 + (long)h3 * (long)_r0 + (long)h4 * (long)_s4 + (long)h5 * (long)_s3); ulong num4 = (ulong)((long)h * (long)_r3 + (long)h2 * (long)_r2 + (long)h3 * (long)_r1 + (long)h4 * (long)_r0 + (long)h5 * (long)_s4); long num5 = (long)h * (long)_r4 + (long)h2 * (long)_r3 + (long)h3 * (long)_r2 + (long)h4 * (long)_r1 + (long)h5 * (long)_r0; ulong num6 = num >> 26; h = (uint)((int)num & 0x3FFFFFF); num2 += num6; num6 = num2 >> 26; h2 = (uint)((int)num2 & 0x3FFFFFF); num3 += num6; num6 = num3 >> 26; h3 = (uint)((int)num3 & 0x3FFFFFF); num4 += num6; num6 = num4 >> 26; h4 = (uint)((int)num4 & 0x3FFFFFF); long num7 = num5 + (long)num6; num6 = (ulong)num7 >> 26; h5 = (uint)((int)num7 & 0x3FFFFFF); h += (uint)((int)num6 * 5); uint num8 = h >> 26; h &= 0x3FFFFFF; h2 += num8; _h0 = h; _h1 = h2; _h2 = h3; _h3 = h4; _h4 = h5; } public void Final(Span<byte> tag) { if (_leftover > 0) { _buffer[_leftover] = 1; for (int i = _leftover + 1; i < 16; i++) { _buffer[i] = 0; } Blocks(_buffer, 0u); _leftover = 0; } uint h = _h0; uint h2 = _h1; uint h3 = _h2; uint h4 = _h3; uint h5 = _h4; uint num = h2 >> 26; h2 &= 0x3FFFFFF; h3 += num; num = h3 >> 26; h3 &= 0x3FFFFFF; h4 += num; num = h4 >> 26; h4 &= 0x3FFFFFF; h5 += num; num = h5 >> 26; h5 &= 0x3FFFFFF; h += num * 5; num = h >> 26; h &= 0x3FFFFFF; h2 += num; uint num2 = h + 5; num = num2 >> 26; num2 &= 0x3FFFFFF; uint num3 = h2 + num; num = num3 >> 26; num3 &= 0x3FFFFFF; uint num4 = h3 + num; num = num4 >> 26; num4 &= 0x3FFFFFF; uint num5 = h4 + num; num = num5 >> 26; num5 &= 0x3FFFFFF; uint num6 = h5 + num - 67108864; uint num7 = (num6 >> 31) - 1; num2 &= num7; num3 &= num7; num4 &= num7; num5 &= num7; num6 &= num7; num7 = ~num7; h = (h & num7) | num2; h2 = (h2 & num7) | num3; h3 = (h3 & num7) | num4; h4 = (h4 & num7) | num5; h5 = (h5 & num7) | num6; h |= h2 << 26; h2 = (h2 >> 6) | (h3 << 20); h3 = (h3 >> 12) | (h4 << 14); h4 = (h4 >> 18) | (h5 << 8); ulong num8 = (ulong)h + (ulong)_pad0; h = (uint)num8; num8 = (ulong)((long)h2 + (long)_pad1) + (num8 >> 32); h2 = (uint)num8; num8 = (ulong)((long)h3 + (long)_pad2) + (num8 >> 32); h3 = (uint)num8; num8 = (ulong)((long)h4 + (long)_pad3) + (num8 >> 32); h4 = (uint)num8; ChaCha20Poly1305.StoreLE32(tag, 0, h); ChaCha20Poly1305.StoreLE32(tag, 4, h2); ChaCha20Poly1305.StoreLE32(tag, 8, h3); ChaCha20Poly1305.StoreLE32(tag, 12, h4); _h0 = (_h1 = (_h2 = (_h3 = (_h4 = 0u)))); } } public static class CryptoUtil { public static bool FixedTimeEquals(ReadOnlySpan<byte> a, ReadOnlySpan<byte> b) { if (a.Length != b.Length) { return false; } int num = 0; for (int i = 0; i < a.Length; i++) { num |= a[i] ^ b[i]; } return num == 0; } public static void Zero(Span<byte> data) { data.Clear(); } public static string ToHex(ReadOnlySpan<byte> data) { char[] array = new char[data.Length * 2]; for (int i = 0; i < data.Length; i++) { array[2 * i] = "0123456789abcdef"[data[i] >> 4]; array[2 * i + 1] = "0123456789abcdef"[data[i] & 0xF]; } return new string(array); } public static byte[] FromHex(string hex) { if (hex == null || (hex.Length & 1) != 0) { throw new FormatException("hex string must have even length"); } byte[] array = new byte[hex.Length / 2]; for (int i = 0; i < array.Length; i++) { array[i] = (byte)((Nibble(hex[2 * i]) << 4) | Nibble(hex[2 * i + 1])); } return array; } private static int Nibble(char c) { if (c >= '0' && c <= '9') { return c - 48; } if (c >= 'a' && c <= 'f') { return c - 97 + 10; } if (c >= 'A' && c <= 'F') { return c - 65 + 10; } throw new FormatException("invalid hex character"); } public static string Fingerprint(ReadOnlySpan<byte> publicKey) { string text = ToHex(Sha256.Hash(publicKey).AsSpan(0, 16)); string[] array = new string[8]; for (int i = 0; i < 8; i++) { array[i] = text.Substring(i * 4, 4); } return string.Join(":", array); } } public static class SecureRandom { private static int _mode; private static readonly object Lock = new object(); private static FileStream _urandom; public static void Fill(Span<byte> data) { if (_mode != 2) { try { RandomNumberGenerator.Fill(data); _mode = 1; return; } catch (Exception) when (_mode == 0) { _mode = 2; } } lock (Lock) { if (_urandom == null) { _urandom = new FileStream("/dev/urandom", FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 1); } int num; for (int i = 0; i < data.Length; i += num) { num = _urandom.Read(data.Slice(i)); if (num <= 0) { throw new IOException("/dev/urandom returned no data"); } } } } public static byte[] Bytes(int count) { byte[] array = new byte[count]; Fill(array); return array; } public static uint NextUInt32() { Span<byte> data = stackalloc byte[4]; Fill(data); return (uint)(data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24)); } public static ulong NextUInt64() { Span<byte> data = stackalloc byte[8]; Fill(data); ulong num = 0uL; for (int i = 0; i < 8; i++) { num |= (ulong)data[i] << 8 * i; } return num; } } public sealed class Sha256 { public const int HashSize = 32; public const int BlockSize = 64; private static readonly uint[] K = new uint[64] { 1116352408u, 1899447441u, 3049323471u, 3921009573u, 961987163u, 1508970993u, 2453635748u, 2870763221u, 3624381080u, 310598401u, 607225278u, 1426881987u, 1925078388u, 2162078206u, 2614888103u, 3248222580u, 3835390401u, 4022224774u, 264347078u, 604807628u, 770255983u, 1249150122u, 1555081692u, 1996064986u, 2554220882u, 2821834349u, 2952996808u, 3210313671u, 3336571891u, 3584528711u, 113926993u, 338241895u, 666307205u, 773529912u, 1294757372u, 1396182291u, 1695183700u, 1986661051u, 2177026350u, 2456956037u, 2730485921u, 2820302411u, 3259730800u, 3345764771u, 3516065817u, 3600352804u, 4094571909u, 275423344u, 430227734u, 506948616u, 659060556u, 883997877u, 958139571u, 1322822218u, 1537002063u, 1747873779u, 1955562222u, 2024104815u, 2227730452u, 2361852424u, 2428436474u, 2756734187u, 3204031479u, 3329325298u }; private readonly uint[] _h = new uint[8]; private readonly uint[] _w = new uint[64]; private readonly byte[] _buffer = new byte[64]; private int _bufferLength; private ulong _totalLength; public Sha256() { Reset(); } public void Reset() { _h[0] = 1779033703u; _h[1] = 3144134277u; _h[2] = 1013904242u; _h[3] = 2773480762u; _h[4] = 1359893119u; _h[5] = 2600822924u; _h[6] = 528734635u; _h[7] = 1541459225u; _bufferLength = 0; _totalLength = 0uL; } public void Update(ReadOnlySpan<byte> data) { _totalLength += (ulong)data.Length; if (_bufferLength > 0) { int num = Math.Min(64 - _bufferLength, data.Length); data.Slice(0, num).CopyTo(_buffer.AsSpan(_bufferLength)); _bufferLength += num; data = data.Slice(num); if (_bufferLength < 64) { return; } Compress(_buffer); _bufferLength = 0; } while (data.Length >= 64) { Compress(data.Slice(0, 64)); data = data.Slice(64); } data.CopyTo(_buffer); _bufferLength = data.Length; } public void Final(Span<byte> output) { if (output.Length < 32) { throw new ArgumentException("output too small"); } ulong num = _totalLength * 8; Span<byte> span = stackalloc byte[128]; span.Clear(); span[0] = 128; int num2 = ((_bufferLength < 56) ? (56 - _bufferLength) : (120 - _bufferLength)); for (int i = 0; i < 8; i++) { span[num2 + i] = (byte)(num >> 56 - 8 * i); } ulong totalLength = _totalLength; Update(span.Slice(0, num2 + 8)); _totalLength = totalLength; for (int j = 0; j < 8; j++) { output[4 * j] = (byte)(_h[j] >> 24); output[4 * j + 1] = (byte)(_h[j] >> 16); output[4 * j + 2] = (byte)(_h[j] >> 8); output[4 * j + 3] = (byte)_h[j]; } Reset(); } public static byte[] Hash(ReadOnlySpan<byte> data) { Sha256 sha = new Sha256(); sha.Update(data); byte[] array = new byte[32]; sha.Final(array); return array; } private static uint Rotr(uint x, int n) { return (x >> n) | (x << 32 - n); } private void Compress(ReadOnlySpan<byte> block) { uint[] w = _w; for (int i = 0; i < 16; i++) { w[i] = (uint)((block[4 * i] << 24) | (block[4 * i + 1] << 16) | (block[4 * i + 2] << 8) | block[4 * i + 3]); } for (int j = 16; j < 64; j++) { uint num = Rotr(w[j - 15], 7) ^ Rotr(w[j - 15], 18) ^ (w[j - 15] >> 3); uint num2 = Rotr(w[j - 2], 17) ^ Rotr(w[j - 2], 19) ^ (w[j - 2] >> 10); w[j] = w[j - 16] + num + w[j - 7] + num2; } uint num3 = _h[0]; uint num4 = _h[1]; uint num5 = _h[2]; uint num6 = _h[3]; uint num7 = _h[4]; uint num8 = _h[5]; uint num9 = _h[6]; uint num10 = _h[7]; for (int k = 0; k < 64; k++) { uint num11 = Rotr(num7, 6) ^ Rotr(num7, 11) ^ Rotr(num7, 25); uint num12 = (num7 & num8) ^ (~num7 & num9); uint num13 = num10 + num11 + num12 + K[k] + w[k]; uint num14 = Rotr(num3, 2) ^ Rotr(num3, 13) ^ Rotr(num3, 22); uint num15 = (num3 & num4) ^ (num3 & num5) ^ (num4 & num5); uint num16 = num14 + num15; num10 = num9; num9 = num8; num8 = num7; num7 = num6 + num13; num6 = num5; num5 = num4; num4 = num3; num3 = num13 + num16; } _h[0] += num3; _h[1] += num4; _h[2] += num5; _h[3] += num6; _h[4] += num7; _h[5] += num8; _h[6] += num9; _h[7] += num10; } } public static class Hkdf { public static byte[] HmacSha256(ReadOnlySpan<byte> key, ReadOnlySpan<byte> message) { Span<byte> span = stackalloc byte[64]; span.Clear(); if (key.Length > 64) { Sha256.Hash(key).CopyTo(span); } else { key.CopyTo(span); } Span<byte> span2 = stackalloc byte[64]; Sha256 sha = new Sha256(); for (int i = 0; i < span2.Length; i++) { span2[i] = (byte)(span[i] ^ 0x36); } sha.Update(span2); sha.Update(message); Span<byte> span3 = stackalloc byte[32]; sha.Final(span3); Sha256 sha2 = new Sha256(); for (int j = 0; j < span2.Length; j++) { span2[j] = (byte)(span[j] ^ 0x5C); } sha2.Update(span2); sha2.Update(span3); byte[] array = new byte[32]; sha2.Final(array); CryptoUtil.Zero(span); return array; } public static byte[] Extract(ReadOnlySpan<byte> salt, ReadOnlySpan<byte> ikm) { return HmacSha256((salt.Length == 0) ? ((ReadOnlySpan<byte>)new byte[32]) : salt, ikm); } public static byte[] Expand(ReadOnlySpan<byte> prk, ReadOnlySpan<byte> info, int length) { if (length <= 0 || length > 8160) { throw new ArgumentOutOfRangeException("length"); } byte[] array = new byte[length]; byte[] array2 = Array.Empty<byte>(); int num = 0; byte b = 1; while (num < length) { byte[] array3 = new byte[array2.Length + info.Length + 1]; array2.CopyTo(array3, 0); info.CopyTo(array3.AsSpan(array2.Length)); array3[^1] = b; array2 = HmacSha256(prk, array3); int num2 = Math.Min(array2.Length, length - num); Array.Copy(array2, 0, array, num, num2); num += num2; b++; } return array; } public static byte[] DeriveKey(ReadOnlySpan<byte> salt, ReadOnlySpan<byte> ikm, ReadOnlySpan<byte> info, int length) { byte[] array = Extract(salt, ikm); try { return Expand(array, info, length); } finally { CryptoUtil.Zero(array); } } } public static class X25519 { public const int KeySize = 32; private static readonly long[] A24 = new long[16] { 56129L, 1L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L }; private static readonly byte[] BasePoint = CreateBasePoint(); [ThreadStatic] private static long[] _mulScratch; private static byte[] CreateBasePoint() { byte[] array = new byte[32]; array[0] = 9; return array; } public static void GeneratePrivateKey(Span<byte> privateKey) { SecureRandom.Fill(privateKey.Slice(0, 32)); privateKey[0] &= 248; privateKey[31] &= 127; privateKey[31] |= 64; } public static byte[] GeneratePrivateKey() { byte[] array = new byte[32]; GeneratePrivateKey(array); return array; } public static byte[] PublicKeyFromPrivate(ReadOnlySpan<byte> privateKey) { byte[] array = new byte[32]; ScalarMult(array, privateKey, BasePoint); return array; } public static bool SharedSecret(Span<byte> output, ReadOnlySpan<byte> privateKey, ReadOnlySpan<byte> peerPublic) { if (peerPublic.Length != 32 || privateKey.Length != 32) { throw new ArgumentException("bad key size"); } ScalarMult(output, privateKey, peerPublic); int num = 0; for (int i = 0; i < 32; i++) { num |= output[i]; } if (num == 0) { output.Slice(0, 32).Clear(); return false; } return true; } public static void ScalarMult(Span<byte> q, ReadOnlySpan<byte> n, ReadOnlySpan<byte> p) { Span<byte> span = stackalloc byte[32]; long[] array = new long[80]; long[] array2 = new long[16]; long[] array3 = new long[16]; long[] array4 = new long[16]; long[] array5 = new long[16]; long[] array6 = new long[16]; long[] array7 = new long[16]; for (int i = 0; i < 31; i++) { span[i] = n[i]; } span[31] = (byte)((n[31] & 0x7F) | 0x40); span[0] &= 248; Unpack(array, 0, p); for (int j = 0; j < 16; j++) { array3[j] = array[j]; array5[j] = (array2[j] = (array4[j] = 0L)); } array2[0] = (array5[0] = 1L); long[] array8 = new long[16]; Array.Copy(array, 0, array8, 0, 16); for (int num = 254; num >= 0; num--) { long b = (span[num >> 3] >> (num & 7)) & 1; Sel(array2, array3, b); Sel(array4, array5, b); Add(array6, array2, array4); Sub(array2, array2, array4); Add(array4, array3, array5); Sub(array3, array3, array5); Sqr(array5, array6); Sqr(array7, array2); Mul(array2, array4, array2); Mul(array4, array3, array6); Add(array6, array2, array4); Sub(array2, array2, array4); Sqr(array3, array2); Sub(array4, array5, array7); Mul(array2, array4, A24); Add(array2, array2, array5); Mul(array4, array4, array2); Mul(array2, array5, array7); Mul(array5, array3, array8); Sqr(array3, array6); Sel(array2, array3, b); Sel(array4, array5, b); } Inv(array4, array4); Mul(array2, array2, array4); Pack(q, array2); span.Clear(); Array.Clear(array, 0, array.Length); Array.Clear(array2, 0, 16); Array.Clear(array3, 0, 16); Array.Clear(array4, 0, 16); Array.Clear(array5, 0, 16); Array.Clear(array6, 0, 16); Array.Clear(array7, 0, 16); } private static void Carry(long[] o) { for (int i = 0; i < 16; i++) { o[i] += 65536L; long num = o[i] >> 16; if (i < 15) { o[i + 1] += num - 1; } else { o[0] += 38 * (num - 1); } o[i] -= num << 16; } } private static void Sel(long[] p, long[] q, long b) { long num = ~(b - 1); for (int i = 0; i < 16; i++) { long num2 = num & (p[i] ^ q[i]); p[i] ^= num2; q[i] ^= num2; } } private static void Pack(Span<byte> o, long[] n) { long[] array = new long[16]; long[] array2 = new long[16]; Array.Copy(n, array2, 16); Carry(array2); Carry(array2); Carry(array2); for (int i = 0; i < 2; i++) { array[0] = array2[0] - 65517; for (int j = 1; j < 15; j++) { array[j] = array2[j] - 65535 - ((array[j - 1] >> 16) & 1); array[j - 1] &= 65535L; } array[15] = array2[15] - 32767 - ((array[14] >> 16) & 1); long num = (array[15] >> 16) & 1; array[14] &= 65535L; Sel(array2, array, 1 - num); } for (int k = 0; k < 16; k++) { o[2 * k] = (byte)(array2[k] & 0xFF); o[2 * k + 1] = (byte)(array2[k] >> 8); } } private static void Unpack(long[] o, int offset, ReadOnlySpan<byte> n) { for (int i = 0; i < 16; i++) { o[offset + i] = (long)(n[2 * i] + ((ulong)n[2 * i + 1] << 8)); } o[offset + 15] &= 32767L; } private static void Add(long[] o, long[] a, long[] b) { for (int i = 0; i < 16; i++) { o[i] = a[i] + b[i]; } } private static void Sub(long[] o, long[] a, long[] b) { for (int i = 0; i < 16; i++) { o[i] = a[i] - b[i]; } } private static void Mul(long[] o, long[] a, long[] b) { long[] array = _mulScratch ?? (_mulScratch = new long[31]); Array.Clear(array, 0, 31); for (int i = 0; i < 16; i++) { long num = a[i]; for (int j = 0; j < 16; j++) { array[i + j] += num * b[j]; } } for (int k = 0; k < 15; k++) { array[k] += 38 * array[k + 16]; } Array.Copy(array, o, 16); Carry(o); Carry(o); } private static void Sqr(long[] o, long[] a) { Mul(o, a, a); } private static void Inv(long[] o, long[] i) { long[] array = new long[16]; Array.Copy(i, array, 16); for (int num = 253; num >= 0; num--) { Sqr(array, array); if (num != 2 && num != 4) { Mul(array, array, i); } } Array.Copy(array, o, 16); } } } namespace LabFusion.Network.Fischyweb { public sealed class FischywebMatchmaker : IMatchmaker { private readonly FischywebNetworkLayer _layer; public FischywebMatchmaker(FischywebNetworkLayer layer) { _layer = layer; } public void RequestLobbies(Action<MatchmakerCallbackInfo> callback) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) RequestLobbies(MatchmakerFilters.Empty, callback); } public void RequestLobbies(MatchmakerFilters filters, Action<MatchmakerCallbackInfo> callback) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) LobbyQuery lobbyQuery = AddPersistentFilters(new LobbyQuery()); if (filters.FilterFull) { lobbyQuery.WithEqual("Full", bool.FalseString); } if (filters.FilterMismatchingVersions) { Version version = FusionMod.Version; lobbyQuery.WithEqual("VersionMajor", version.Major.ToString()).WithEqual("VersionMinor", version.Minor.ToString()); } lobbyQuery.WithNotEqual("Privacy", 1.ToString()).WithNotEqual("Privacy", 3.ToString()); Search(lobbyQuery, callback); } public void RequestLobbiesByCode(string code, Action<MatchmakerCallbackInfo> callback) { LobbyQuery query = AddPersistentFilters(new LobbyQuery()).WithEqual("LobbyCode", (code ?? string.Empty).Trim().ToUpperInvariant()); Search(query, callback); } private static LobbyQuery AddPersistentFilters(LobbyQuery query) { return query.WithEqual("MarrowFusion", bool.TrueString).WithEqual("HasLobbyOpen", bool.TrueString).WithEqual("Game", GameInfo.GameName); } private void Search(LobbyQuery query, Action<MatchmakerCallbackInfo> callback) { _layer.RequestLobbies(query, delegate(List<LobbyEntry> entries) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) List<LobbyInfo> list = new List<LobbyInfo>(entries.Count); foreach (LobbyEntry entry in entries) { if (entry.LobbyId != _layer.LocalPlatformID) { FischywebNetworkLobby fischywebNetworkLobby = new FischywebNetworkLobby(entry.Metadata); LobbyMetadataInfo metadata = LobbyMetadataSerializer.ReadInfo((INetworkLobby)(object)fischywebNetworkLobby); if (((LobbyMetadataInfo)(ref metadata)).HasLobbyOpen) { list.Add(new LobbyInfo { Lobby = (INetworkLobby)(object)fischywebNetworkLobby, Metadata = metadata }); } } } callback?.Invoke(new MatchmakerCallbackInfo { Lobbies = list.ToArray() }); }); } } public class FischywebNetworkLayer : NetworkLayer { private struct LocalMessage { public byte[] Data; public bool IsServerHandled; } private class LobbyRequest { public Action<List<LobbyEntry>> Callback; public float TimeRemaining; } public const string LayerTitle = "Fischyweb"; private const float LobbySearchTimeout = 20f; private FischywebNetworkLobby _currentLobby; private IVoiceManager _voiceManager; private IMatchmaker _matchmaker; private FischywebSettings _settings; private RelayClient _client; private bool _isServerActive; private bool _isConnectionActive; private ulong _hostId; private bool _wantOnline; private bool _loggedIn; private bool _connecting; private int _reconnectAttempt; private readonly Queue<LocalMessage> _localMessages = new Queue<LocalMessage>(); private readonly Dictionary<uint, LobbyRequest> _lobbyRequests = new Dictionary<uint, LobbyRequest>(); public static FischywebNetworkLayer Instance { get; private set; } public override string Title => "Fischyweb"; public override string Platform => "Fischyweb"; public override bool IsHost => _isServerActive; public override bool IsClient => _isConnectionActive; public override INetworkLobby Lobby => (INetworkLobby)(object)_currentLobby; public override IVoiceManager VoiceManager => _voiceManager; public override IMatchmaker Matchmaker => _matchmaker; public ulong LocalPlatformID { get; private set; } public bool IsRelayConnected { get { if (_client != null) { return !_client.IsClosed; } return false; } } public string ServerCode { get; private set; } public override bool CheckSupported() { return true; } public override bool CheckValidation() { return true; } public override void OnInitializeLayer() { //IL_001b: Unknown result type (might be due to invalid IL or missing references)