using axXez.Threading; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Numerics; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace axXez.TS3.Protocol { /// TeamSpeak 3 UDP voice client. Handles the full handshake and packet crypto. public class VoiceClient : LoopTaskHandler, ITeamspeakClient { private const int C2sHeaderLen = 5; // PId(2) + CId(2) + PT(1) private const int S2cHeaderLen = 3; // PId(2) + PT(1) private const int MaxPacketSize = 500; private const int MaxOutContent = MaxPacketSize - VoiceCrypto.MacLen - C2sHeaderLen; private const byte FlagFragmented = 0x10; private const byte FlagNewProtocol = 0x20; private const byte FlagUnencrypted = 0x80; // TS3AudioBot version signature accepted by all TS3 servers private static readonly string VersionSignPlatform = "Linux"; private static readonly string VersionSignVersion = "3.?.? [Build: 5680278000]"; private static readonly string VersionSignSign = "Hjd+N58Gv3ENhoKmGYy2bNRBsNNgm5kpiaQWxOj5HN2DXttG6REjymSwJtpJ8muC2gSwRuZi0R+8Laan5ts5CQ=="; /// Handler raised when a transport or protocol error occurs. public delegate void VoiceErrorHandler(VoiceClient sender, Exception ex); /// Handler raised when a command packet is received from the server. public delegate void VoiceCommandHandler(VoiceClient sender, TsMessage command); /// Handler raised when a decoded voice packet is received. public delegate void VoiceDataHandler(VoiceClient sender, byte[] data); /// Handler raised when this client is moved to a different channel. public delegate void VoiceChannelChangedHandler(VoiceClient sender, int newChannelId); /// public event TeamspeakClientHandler OnConnected; /// public event TeamspeakClientHandler OnDisconnected; /// Raised when a transport or protocol error occurs. public event VoiceErrorHandler OnError; /// Raised when a command packet is received from the server. public event VoiceCommandHandler OnCommand; /// Raised when a decoded voice packet is received. public event VoiceDataHandler OnVoiceData; /// Raised when this client is moved to a different channel. public event VoiceChannelChangedHandler OnChannelChanged; /// public event TeamspeakClientHandler OnTextMessage; /// Current connection state of the voice client. public VoiceClientState State { get; private set; } = VoiceClientState.Disconnected; /// Client ID assigned by the server after a successful handshake. public int ClientId { get; private set; } /// Channel ID this client is currently in. Updated on every channel move. public int CurrentChannelId { get; private set; } /// public string Host => _host; /// public int Port => _port; /// Display nickname this client uses on the server. public string Nickname => _nickname; /// public bool IsConnected => State == VoiceClientState.Connected; /// Milliseconds to wait before each reconnect attempt. Default is 5000. public int ReconnectDelay { get; set; } = 5000; private readonly LoopTask resendProcessor; private readonly LoopTask commandProcessor; private readonly ConcurrentQueue _commandQueue = new(); private readonly string _host; private readonly int _port; private readonly string _nickname; private readonly VoiceIdentity _identity; private readonly VoiceClientOptions _opts; private UdpClient _udp; // Per-type packet counters for outgoing (C2S) and incoming (S2C) directions private readonly ushort[] _outCounters = new ushort[9]; private readonly uint[] _outGenerations = new uint[9]; private readonly ushort[] _inCounters = new ushort[9]; private readonly uint[] _inGenerations = new uint[9]; // Resend tracking private readonly Dictionary _resendMap = new(); private (byte[] raw, long firstSend, long lastSend)? _initResend; private long _lastMessageTick; private long _lastPingTick; // Crypto state private bool _cryptoInitComplete; private byte[] _ivStruct; private readonly byte[] _fakeSignature = new byte[VoiceCrypto.MacLen]; private byte[] _alphaTmp; // Fragment reassembly private readonly List _fragmentBuffer = new(); private bool _fragmenting; private byte _fragmentFlags; // Channel map (name → cid) for auto-move after channellistfinished private readonly Dictionary _channelMap = new(); private IPEndPoint _remoteEndpoint; private readonly object _lock = new(); /// Initializes a new targeting the given host and port. Call to connect. public VoiceClient(string host, int port = 9987, string nickname = "Bot", VoiceIdentity identity = null, VoiceClientOptions options = null) { _host = host; _port = port; _nickname = nickname; _identity = identity ?? VoiceIdentity.Generate(); _opts = options ?? new VoiceClientOptions(); AutoRestart = true; resendProcessor = new(resendLoop, null, resendLoopStop, autoRestart: false); commandProcessor = new(commandLoop, null, commandLoopStop, autoRestart: true); } protected override async Task loopStart(CancellationToken ct, bool isAutoRestart) { if (isAutoRestart) { GlobalLogger.Log("VoiceClient", $"Reconnecting in {ReconnectDelay / 1000}s..."); await Task.Delay(ReconnectDelay, ct); } GlobalLogger.Log("VoiceClient", $"Connecting to {_host}:{_port}..."); ResetState(); // Resolve host once so every Send() uses a pre-resolved IPEndPoint. IPAddress addr = IPAddress.TryParse(_host, out var parsed) ? parsed : (await Dns.GetHostAddressesAsync(_host, AddressFamily.InterNetwork, ct))[0]; _remoteEndpoint = new IPEndPoint(addr, _port); _udp = new UdpClient(0); // 0 = OS assigns random local port _udp.Client.SendBufferSize = 1024 * 1024; _udp.Client.ReceiveBufferSize = 1024 * 1024; _lastMessageTick = Environment.TickCount64; // Increment command counter without sending (matches TS3 client behaviour) IncrementOutCounter(VoicePacketType.Command); resendProcessor.Start(); commandProcessor.Start(); SendInitPacket(BuildInit0()); } protected override async Task loop(CancellationToken ct) { UdpReceiveResult result = await _udp.ReceiveAsync(ct); lock (_lock) _lastMessageTick = Environment.TickCount64; try { HandleIncomingPacket(result.Buffer); } catch (Exception ex) { GlobalLogger.LogError("VoiceClient", "Packet handling failed", ex); OnError?.Invoke(this, ex); } } protected override void loopStop(Exception exception) { commandProcessor.Stop(); resendProcessor.Stop(); try { _udp?.Close(); } catch { } _udp = null; bool wasConnected = State == VoiceClientState.Connected || State == VoiceClientState.Disconnecting; State = VoiceClientState.Disconnected; if (exception != null) GlobalLogger.LogError("VoiceClient", "Connection lost", exception); else GlobalLogger.Log("VoiceClient", "Disconnected."); if (wasConnected) OnDisconnected?.Invoke(this); } /// Sends clientdisconnect to the server and closes the connection after a short drain delay. public void Disconnect() { lock (_lock) { if (State != VoiceClientState.Connected) return; State = VoiceClientState.Disconnecting; } SendCommand("clientdisconnect", new Dictionary { ["reasonid"] = 8, ["reasonmsg"] = "leaving" }); Task.Delay(500).ContinueWith(_ => Stop(wait: false)); } /// Closes the UDP socket to force an immediate reconnect attempt. public void ForceReconnect() { try { _udp?.Close(); } catch { } } /// Sends a pre-encoded Opus audio frame. public void SendVoice(byte[] opusData) { if (State != VoiceClientState.Connected) return; byte[] payload = new byte[3 + opusData.Length]; payload[2] = 5; // Opus Music codec Buffer.BlockCopy(opusData, 0, payload, 3, opusData.Length); SendOutgoing(payload, VoicePacketType.Voice); } /// Sends an end-of-stream voice marker. public void SendVoiceStop() { if (State != VoiceClientState.Connected) return; SendOutgoing(new byte[] { 0, 0, 5 }, VoicePacketType.Voice); } /// Sends a text message. Returns false if not connected. public bool SendTextMessage(MessageTargetMode targetmode, int target, string text) { if (State != VoiceClientState.Connected) return false; SendCommand($"sendtextmessage targetmode={(int)targetmode} target={target} msg={text.ToTsMessageFormat()}"); return true; } /// Sends a text message to the channel this client is currently in. public bool SendChannelMessage(string text) => CurrentChannelId > 0 && SendTextMessage(MessageTargetMode.Channel, CurrentChannelId, text); /// Sends a raw command string to the server. public void SendCommand(string cmd, Dictionary parameters) => SendCommand(TsEncoding.Build(cmd, parameters)); /// Sends a raw command string to the server. public void SendCommand(string cmd, Dictionary parameters) => SendCommand(TsEncoding.Build(cmd, parameters)); /// Sends a raw command string to the server. public void SendCommand(string cmd) { byte[] data = Encoding.UTF8.GetBytes(cmd); if (data.Length <= MaxOutContent) { SendOutgoing(data, VoicePacketType.Command); return; } // Fragment large commands — FLAG_FRAGMENTED on first and last chunk only int chunkCount = (data.Length + MaxOutContent - 1) / MaxOutContent; for (int i = 0; i < chunkCount; i++) { int offset = i * MaxOutContent; int len = Math.Min(MaxOutContent, data.Length - offset); byte[] chunk = new byte[len]; Buffer.BlockCopy(data, offset, chunk, 0, len); byte extraFlags = (i == 0 || i == chunkCount - 1) ? FlagFragmented : (byte)0; SendOutgoing(chunk, VoicePacketType.Command, extraFlags); } } private byte[] BuildInit0() { byte[] buf = new byte[4 + 1 + 4 + 4 + 8]; WriteUInt32BE(buf, 0, VoiceCrypto.InitVersion); buf[4] = 0x00; // step 0 WriteUInt32BE(buf, 5, (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds()); RandomNumberGenerator.Fill(buf.AsSpan(9, 4)); return buf; } private void SendInitPacket(byte[] data) { byte[] raw = BuildRawPacket(data, VoicePacketType.Init1, 101, 0, FlagUnencrypted); VoiceCrypto.InitMac.CopyTo(raw, 0); // overwrite MAC with TS3INIT1 lock (_lock) { _initResend = (raw, Environment.TickCount64, Environment.TickCount64); } SendRaw(raw); } private void SendOutgoing(byte[] data, VoicePacketType type, byte extraFlags = 0) { ushort id; uint gen; lock (_lock) { (id, gen) = GetOutCounter(type); IncrementOutCounter(type); } byte flags = extraFlags; switch (type) { case VoicePacketType.Voice: case VoicePacketType.VoiceWhisper: data[0] = (byte)(id >> 8); data[1] = (byte)id; break; case VoicePacketType.Command: case VoicePacketType.CommandLow: flags |= FlagNewProtocol; break; case VoicePacketType.Ping: case VoicePacketType.Pong: flags |= FlagUnencrypted; break; } byte[] raw = BuildRawPacket(data, type, id, gen, flags); EncryptPacket(raw, type, id, gen, flags, data); if (type == VoicePacketType.Command || type == VoicePacketType.CommandLow) { lock (_lock) _resendMap[id] = (raw, Environment.TickCount64, Environment.TickCount64); } SendRaw(raw); } private byte[] BuildRawPacket(byte[] data, VoicePacketType type, ushort packetId, uint gen, byte flags) { int cid; lock (_lock) cid = ClientId; byte[] raw = new byte[VoiceCrypto.MacLen + C2sHeaderLen + data.Length]; raw[VoiceCrypto.MacLen + 0] = (byte)(packetId >> 8); raw[VoiceCrypto.MacLen + 1] = (byte)packetId; raw[VoiceCrypto.MacLen + 2] = (byte)(cid >> 8); raw[VoiceCrypto.MacLen + 3] = (byte)cid; raw[VoiceCrypto.MacLen + 4] = (byte)((flags & 0xf0) | ((byte)type & 0x0f)); Buffer.BlockCopy(data, 0, raw, VoiceCrypto.MacLen + C2sHeaderLen, data.Length); return raw; } private void EncryptPacket(byte[] raw, VoicePacketType type, ushort packetId, uint gen, byte flags, byte[] data) { if (type == VoicePacketType.Init1) { VoiceCrypto.InitMac.CopyTo(raw, 0); return; } if ((flags & FlagUnencrypted) != 0) { lock (_lock) Buffer.BlockCopy(_fakeSignature, 0, raw, 0, VoiceCrypto.MacLen); return; } byte[] key, nonce; lock (_lock) { if (!_cryptoInitComplete) { key = VoiceCrypto.DummyKey; nonce = VoiceCrypto.DummyNonce; } else (key, nonce) = VoiceCrypto.DeriveKeyNonce(false, packetId, gen, (byte)type, _ivStruct); } var header = raw.AsSpan(VoiceCrypto.MacLen, C2sHeaderLen).ToArray(); var (ciphertext, mac) = VoiceCrypto.EaxEncrypt(key, nonce, header, data); Buffer.BlockCopy(mac, 0, raw, 0, VoiceCrypto.MacLen); Buffer.BlockCopy(ciphertext, 0, raw, VoiceCrypto.MacLen + C2sHeaderLen, ciphertext.Length); } private void SendRaw(byte[] raw) { UdpClient udp; lock (_lock) udp = _udp; if (udp == null || State == VoiceClientState.Disconnected) return; try { udp.Send(raw, raw.Length, _remoteEndpoint); } catch (Exception ex) { GlobalLogger.LogError("VoiceClient", "Send failed", ex); OnError?.Invoke(this, ex); } } private void HandleIncomingPacket(byte[] raw) { if (raw.Length < VoiceCrypto.MacLen + S2cHeaderLen) return; ushort packetId = (ushort)((raw[VoiceCrypto.MacLen] << 8) | raw[VoiceCrypto.MacLen + 1]); byte ptByte = raw[VoiceCrypto.MacLen + 2]; var type = (VoicePacketType)(ptByte & 0x0f); byte flags = (byte)(ptByte & 0xf0); byte[] data = DecryptPacket(raw, type, packetId, flags); if (data == null) { GlobalLogger.Log("VoiceClient", $"Failed to decrypt packet type={type} id={packetId}", LogType.Verbose); return; } switch (type) { case VoicePacketType.Init1: HandleInit(data); break; case VoicePacketType.Command: case VoicePacketType.CommandLow: SendAck(packetId, type == VoicePacketType.Command ? VoicePacketType.Ack : VoicePacketType.AckLow); HandleCommandData(data, flags); break; case VoicePacketType.Ack: case VoicePacketType.AckLow: HandleAck(data); break; case VoicePacketType.Ping: HandlePing(packetId); break; case VoicePacketType.Voice: case VoicePacketType.VoiceWhisper: OnVoiceData?.Invoke(this, data); break; } } private byte[] DecryptPacket(byte[] raw, VoicePacketType type, ushort packetId, byte flags) { if (type == VoicePacketType.Init1) { if (!SpanEquals(raw.AsSpan(0, VoiceCrypto.MacLen), VoiceCrypto.InitMac)) return null; return raw.AsSpan(VoiceCrypto.MacLen + S2cHeaderLen).ToArray(); } if ((flags & FlagUnencrypted) != 0) { lock (_lock) { if (_cryptoInitComplete && !SpanEquals(raw.AsSpan(0, VoiceCrypto.MacLen), _fakeSignature)) return null; } return raw.AsSpan(VoiceCrypto.MacLen + S2cHeaderLen).ToArray(); } uint gen; lock (_lock) gen = IncrementInCounter(type, packetId); // Try real key, then dummy as fallback (handles transition packets) return TryDecrypt(raw, type, packetId, gen, forceDummy: false) ?? TryDecrypt(raw, type, packetId, gen, forceDummy: true); } private byte[] TryDecrypt(byte[] raw, VoicePacketType type, ushort packetId, uint gen, bool forceDummy) { byte[] key, nonce; lock (_lock) { if (forceDummy || !_cryptoInitComplete) { key = VoiceCrypto.DummyKey; nonce = VoiceCrypto.DummyNonce; } else { (key, nonce) = VoiceCrypto.DeriveKeyNonce(true, packetId, gen, (byte)type, _ivStruct); } } return VoiceCrypto.EaxDecrypt(key, nonce, raw.AsSpan(VoiceCrypto.MacLen, S2cHeaderLen), raw.AsSpan(VoiceCrypto.MacLen + S2cHeaderLen), raw.AsSpan(0, VoiceCrypto.MacLen)); } private void SendAck(ushort ackId, VoicePacketType ackType) { byte[] buf = { (byte)(ackId >> 8), (byte)ackId }; SendOutgoing(buf, ackType); } private void HandlePing(ushort packetId) { byte[] buf = { (byte)(packetId >> 8), (byte)packetId }; SendOutgoing(buf, VoicePacketType.Pong); } private void HandleAck(byte[] data) { if (data.Length < 2) return; ushort ackedId = (ushort)((data[0] << 8) | data[1]); lock (_lock) _resendMap.Remove(ackedId); } private void HandleInit(byte[] data) { if (data.Length < 1) return; byte step = data[0]; switch (step) { case 1: { if (data.Length < 21) return; byte[] init2 = new byte[4 + 1 + 16 + 4]; WriteUInt32BE(init2, 0, VoiceCrypto.InitVersion); init2[4] = 0x02; Buffer.BlockCopy(data, 1, init2, 5, 16); lock (_lock) _initResend = null; SendInitPacket(init2); break; } case 3: { if (data.Length < 1 + 64 + 64 + 4 + 100) return; lock (_lock) { State = VoiceClientState.Handshake; _initResend = null; } byte[] x = data.AsSpan(1, 64).ToArray(); byte[] n = data.AsSpan(65, 64).ToArray(); int level = (int)ReadUInt32BE(data, 129); byte[] serverData = data.AsSpan(133, 100).ToArray(); // Solve: y = x^(2^level) mod n var xBig = new BigInteger(x, isUnsigned: true, isBigEndian: true); var nBig = new BigInteger(n, isUnsigned: true, isBigEndian: true); var yBig = BigInteger.ModPow(xBig, BigInteger.Pow(2, level), nBig); // Pad y to 64 bytes (matching x/n size) byte[] y64 = new byte[64]; byte[] yRaw = yBig.ToByteArray(isUnsigned: true, isBigEndian: true); int yDstOff = yRaw.Length < 64 ? 64 - yRaw.Length : 0; Buffer.BlockCopy(yRaw, 0, y64, yDstOff, Math.Min(yRaw.Length, 64)); byte[] alphaTmp = RandomNumberGenerator.GetBytes(10); string alpha = Convert.ToBase64String(alphaTmp); string omega = _identity.PublicKeyString; string initAdd = TsEncoding.Build("clientinitiv", new Dictionary { ["alpha"] = alpha, ["omega"] = omega, ["ot"] = 1, ["ip"] = "" }); byte[] textBytes = Encoding.UTF8.GetBytes(initAdd); byte[] init4 = new byte[4 + 1 + 64 + 64 + 4 + 100 + 64 + textBytes.Length]; WriteUInt32BE(init4, 0, VoiceCrypto.InitVersion); init4[4] = 0x04; Buffer.BlockCopy(x, 0, init4, 5, 64); Buffer.BlockCopy(n, 0, init4, 69, 64); WriteUInt32BE(init4, 133, (uint)level); Buffer.BlockCopy(serverData, 0, init4, 137, 100); Buffer.BlockCopy(y64, 0, init4, 237, 64); Buffer.BlockCopy(textBytes, 0, init4, 301, textBytes.Length); lock (_lock) _alphaTmp = alphaTmp; SendInitPacket(init4); break; } case 0x7f: { lock (_lock) _initResend = null; SendInitPacket(BuildInit0()); break; } } } private void HandleCommandData(byte[] data, byte flags) { if ((flags & FlagFragmented) != 0) { lock (_lock) { if (!_fragmenting) { _fragmenting = true; _fragmentFlags = flags; _fragmentBuffer.Clear(); _fragmentBuffer.Add((byte[])data.Clone()); } else { _fragmentBuffer.Add((byte[])data.Clone()); // Reassemble int totalLen = 0; foreach (var f in _fragmentBuffer) totalLen += f.Length; byte[] merged = new byte[totalLen]; int pos = 0; foreach (var f in _fragmentBuffer) { Buffer.BlockCopy(f, 0, merged, pos, f.Length); pos += f.Length; } byte savedFlags = _fragmentFlags; _fragmenting = false; _fragmentBuffer.Clear(); ProcessCommand(merged, savedFlags); } } return; } lock (_lock) { if (_fragmenting) { _fragmentBuffer.Add((byte[])data.Clone()); return; } } ProcessCommand(data, flags); } private void ProcessCommand(byte[] data, byte flags) { if ((flags & 0x40) != 0) // FLAG_COMPRESSED { try { data = QuickLz.Decompress(data); } catch (Exception ex) { GlobalLogger.Log("VoiceClient", $"Decompression failed: {ex.Message}", LogType.Warning); return; } } string cmdStr = Encoding.UTF8.GetString(data); #if DEBUG_QUERY_NOTIFY && !DEBUG_QUERY_ALL GlobalLogger.Log("VoiceClient_DEBUG", $"Read: {cmdStr}", LogType.Verbose); #endif _commandQueue.Enqueue(TsEncoding.Parse(cmdStr)); } private async Task commandLoop(CancellationToken ct) { if (!_commandQueue.TryDequeue(out TsMessage message)) { await Task.Delay(1, ct); return; } OnCommand?.Invoke(this, message); switch (message.Name) { case "initivexpand": HandleInitIvExpand(message.Params); break; case "initivexpand2": HandleInitIvExpand2(message.Params); break; case "initserver": HandleInitServer(message.Params); break; case "channellist": HandleChannelList(message); break; case "channellistfinished": HandleChannelListFinished(); break; case "notifyclientmoved": if (message.Params.TryGetValue(out var movedClid, "clid") && int.TryParse(movedClid, out int movedId) && movedId == ClientId && message.Params.TryGetValue(out var ctidStr, "ctid") && int.TryParse(ctidStr, out int ctid)) { CurrentChannelId = ctid; OnChannelChanged?.Invoke(this, ctid); } break; case "notifyclientleftview": if (message.Params.TryGetValue(out var clid, "clid") && int.TryParse(clid, out int leftId) && leftId == ClientId && State != VoiceClientState.Disconnecting) { bool banned = message.Params.TryGetValue(out var rid, "reasonid") && rid == "6"; if (banned) Disconnect(); // banned — stop cleanly, don't reconnect else ForceReconnect(); // kicked or server-side timeout — try again } break; case "notifytextmessage": OnTextMessage?.Invoke(this, new TextMessageArgs(message.Params)); break; case "error": { if (!message.Params.TryGetValue(out var idStr, "id")) break; int errId = int.Parse(idStr); if (errId == 0) break; // success string msg = message.Params.TryGetValue(out var m, "msg") ? m : "unknown"; OnError?.Invoke(this, new Exception($"TS3 error {errId}: {msg}")); if (errId == 3329) // banned — fatal Disconnect(); break; } } } private void commandLoopStop(LoopTask sender, Exception exception) { if (exception != null) { GlobalLogger.LogError("VoiceClient", "Command loop error", exception); if (State != VoiceClientState.Connected) ForceReconnect(); // trigger main loop to reconnect if error was during handshake } else _commandQueue.Clear(); } private void HandleInitIvExpand(TsDataEntry p) { lock (_lock) _initResend = null; if (!p.TryGetValue(out var alpha, "alpha") || !p.TryGetValue(out var beta, "beta") || !p.TryGetValue(out var omega, "omega")) { var ex = new Exception("Missing initivexpand parameters."); OnError?.Invoke(this, ex); ForceReconnect(); return; } byte[] alphaBytes = Convert.FromBase64String(alpha); byte[] betaBytes = Convert.FromBase64String(beta); byte[] omegaBytes = Convert.FromBase64String(omega); byte[] sharedKey = _identity.GetSharedSecret(omegaBytes); // ivStruct = sharedKey[0..9] XOR alpha[0..9] || sharedKey[10..] XOR beta byte[] iv = new byte[10 + betaBytes.Length]; Buffer.BlockCopy(sharedKey, 0, iv, 0, 10); VoiceCrypto.XorInto(iv, alphaBytes, 10); for (int i = 0; i < betaBytes.Length; i++) iv[10 + i] = (byte)(sharedKey[10 + i] ^ betaBytes[i]); byte[] sig = SHA1.HashData(iv); lock (_lock) { _ivStruct = iv; Buffer.BlockCopy(sig, 0, _fakeSignature, 0, VoiceCrypto.MacLen); _cryptoInitComplete = true; _alphaTmp = null; } SendClientInit(); } private void HandleInitIvExpand2(TsDataEntry p) { byte[] alphaTmp; lock (_lock) { if (_cryptoInitComplete || _alphaTmp == null) { return; } _initResend = null; alphaTmp = _alphaTmp; } if (!p.TryGetValue(out var l, "l") || !p.TryGetValue(out var betaB64, "beta")) { var ex = new Exception("Missing initivexpand2 parameters."); OnError?.Invoke(this, ex); ForceReconnect(); return; } try { byte[] licenseBytes = Convert.FromBase64String(l); byte[] betaBytes = Convert.FromBase64String(betaB64); var blocks = VoiceLicense.ParseLicense(licenseBytes); byte[] serverKey = VoiceLicense.DeriveLicenseKey(blocks); var (tempPub, tempPriv) = VoiceLicense.GenerateTemporaryKey(); // Proof: P-256 ECDSA signature of (tempPub || beta) with the identity key byte[] toSign = new byte[tempPub.Length + betaBytes.Length]; Buffer.BlockCopy(tempPub, 0, toSign, 0, tempPub.Length); Buffer.BlockCopy(betaBytes, 0, toSign, tempPub.Length, betaBytes.Length); SendCommand("clientek", new Dictionary { ["ek"] = Convert.ToBase64String(tempPub), ["proof"] = Convert.ToBase64String(_identity.Sign(toSign)), }); // ivStruct = SHA-512(scalar·serverKey) [64 bytes] byte[] iv = VoiceLicense.GetSharedSecret2(serverKey, tempPriv); // XOR first 10 bytes with alphaTmp, remaining bytes with beta VoiceCrypto.XorInto(iv, alphaTmp, 10); for (int i = 0; i < betaBytes.Length; i++) iv[10 + i] ^= betaBytes[i]; byte[] sig = SHA1.HashData(iv); lock (_lock) { _ivStruct = iv; Buffer.BlockCopy(sig, 0, _fakeSignature, 0, VoiceCrypto.MacLen); _cryptoInitComplete = true; _alphaTmp = null; } SendClientInit(); } catch (Exception ex) { OnError?.Invoke(this, ex); ForceReconnect(); } } private void SendClientInit() { string cmd = TsEncoding.Build("clientinit", new Dictionary { ["client_nickname"] = _nickname, ["client_version"] = VersionSignVersion, ["client_platform"] = VersionSignPlatform, ["client_input_hardware"] = 1, ["client_output_hardware"] = 1, ["client_default_channel"] = _opts.DefaultChannel ?? "", ["client_default_channel_password"] = _opts.ChannelPassword != null ? VoiceCrypto.HashPassword(_opts.ChannelPassword) : "", ["client_server_password"] = _opts.ServerPassword != null ? VoiceCrypto.HashPassword(_opts.ServerPassword) : "", ["client_meta_data"] = "", ["client_version_sign"] = VersionSignSign, ["client_key_offset"] = _identity.KeyOffset.ToString(), ["client_nickname_phonetic"] = "", ["client_default_token"] = "", ["hwid"] = "", }); SendCommand(cmd); } private void HandleInitServer(TsDataEntry p) { int cid = p.GetValue("aclid"); lock (_lock) { ClientId = cid; State = VoiceClientState.Connected; } GlobalLogger.Log("VoiceClient", "Connected."); OnConnected?.Invoke(this); } private void HandleChannelList(TsMessage parsed) { lock (_lock) { foreach (var e in parsed.Entries) { if (e.TryGetValue(out var cidStr, "cid") && int.TryParse(cidStr, out int cid) && e.TryGetValue(out var name, "channel_name") && !string.IsNullOrEmpty(name)) _channelMap[name] = cid; } } } private void HandleChannelListFinished() { string target = _opts.DefaultChannel; if (string.IsNullOrEmpty(target)) return; int cid; lock (_lock) { if (int.TryParse(target, out int numericCid)) cid = numericCid; else if (!_channelMap.TryGetValue(target, out cid)) cid = 0; } if (cid == 0) { GlobalLogger.Log("VoiceClient", $"Default channel \"{target}\" not found.", LogType.Warning); return; } SendCommand("clientmove", new Dictionary { ["cid"] = cid, ["clid"] = ClientId, ["cpw"] = _opts.ChannelPassword ?? "", }); } private async Task resendLoop(CancellationToken ct) { await Task.Delay(100, ct); long now = Environment.TickCount64; if (State == VoiceClientState.Connected && now - _lastPingTick >= 1000) { SendOutgoing(Array.Empty(), VoicePacketType.Ping); _lastPingTick = now; } lock (_lock) { if (_initResend.HasValue) { var ir = _initResend.Value; if (now - ir.firstSend > 30000) { var ex = new Exception("Init timeout."); GlobalLogger.LogError("VoiceClient", "Init timed out", ex); OnError?.Invoke(this, ex); goto cleanup; } if (now - ir.lastSend > 1000) { _initResend = (ir.raw, ir.firstSend, now); SendRaw(ir.raw); } } var timedOut = new List(); foreach (var kv in _resendMap) { if (now - kv.Value.firstSend > 30000) timedOut.Add(kv.Key); else if (now - kv.Value.lastSend > 1000) { _resendMap[kv.Key] = (kv.Value.raw, kv.Value.firstSend, now); SendRaw(kv.Value.raw); } } if (timedOut.Count > 0) { foreach (var id in timedOut) _resendMap.Remove(id); var ex = new Exception($"Packet(s) {string.Join(",", timedOut)} timed out."); GlobalLogger.LogError("VoiceClient", "Packet ACK timeout", ex); OnError?.Invoke(this, ex); goto cleanup; } if (now - _lastMessageTick > 30000) { var ex = new Exception("Connection timeout — no response from server."); GlobalLogger.LogError("VoiceClient", "Connection timed out", ex); OnError?.Invoke(this, ex); goto cleanup; } return; cleanup:; } ForceReconnect(); } private void resendLoopStop(LoopTask sender, Exception exception) { if (exception != null) GlobalLogger.LogError("VoiceClient", "Resend loop error", exception); } private void ResetState() { State = VoiceClientState.Init; ClientId = 0; CurrentChannelId = 0; _cryptoInitComplete = false; _ivStruct = null; Array.Clear(_fakeSignature, 0, _fakeSignature.Length); _alphaTmp = null; Array.Clear(_outCounters, 0, _outCounters.Length); Array.Clear(_outGenerations, 0, _outGenerations.Length); Array.Clear(_inCounters, 0, _inCounters.Length); Array.Clear(_inGenerations, 0, _inGenerations.Length); _resendMap.Clear(); _initResend = null; _fragmentBuffer.Clear(); _fragmenting = false; _fragmentFlags = 0; _channelMap.Clear(); _lastPingTick = 0; } private (ushort id, uint gen) GetOutCounter(VoicePacketType type) { if (type == VoicePacketType.Init1) return (101, 0); int i = (int)type; return (_outCounters[i], _outGenerations[i]); } private void IncrementOutCounter(VoicePacketType type) { if (type == VoicePacketType.Init1) return; int i = (int)type; _outCounters[i]++; if (_outCounters[i] == 0) _outGenerations[i]++; } private uint IncrementInCounter(VoicePacketType type, ushort packetId) { int i = (int)type; if (_inCounters[i] > 0xFF00 && packetId < 0x0100) _inGenerations[i]++; _inCounters[i] = packetId; return _inGenerations[i]; } private static void WriteUInt32BE(byte[] buf, int offset, uint value) { buf[offset + 0] = (byte)(value >> 24); buf[offset + 1] = (byte)(value >> 16); buf[offset + 2] = (byte)(value >> 8); buf[offset + 3] = (byte)value; } private static uint ReadUInt32BE(byte[] buf, int offset) => ((uint)buf[offset] << 24) | ((uint)buf[offset + 1] << 16) | ((uint)buf[offset + 2] << 8) | buf[offset + 3]; private static bool SpanEquals(ReadOnlySpan a, ReadOnlySpan b) => a.SequenceEqual(b); } }