diff --git a/internal/client/client.go b/internal/client/client.go index 7f44671..dda5be1 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1,17 +1,11 @@ package client import ( - "bytes" - "encoding/binary" - "fmt" "log" - "strings" "time" "go-ts/pkg/protocol" "go-ts/pkg/transport" - - "github.com/dgryski/go-quicklz" ) type Channel struct { @@ -58,7 +52,9 @@ func (c *Client) Connect(address string) error { return err } c.Conn = conn - // Initialize handshake state + log.Printf("Connected to UDP. Starting Handshake...") + + // Initialize Handshake State hs, err := NewHandshakeState(c.Conn) if err != nil { return err @@ -68,630 +64,42 @@ func (c *Client) Connect(address string) error { // Improve Identity Security Level to 8 (Standard Requirement) c.Handshake.ImproveSecurityLevel(8) - log.Println("Connected to UDP. Starting Handshake...") - - // Start Handshake Flow - // Step 0 + // Send Init1 if err := c.Handshake.SendPacket0(); err != nil { return err } - // Read Loop for Handshake - timeout := time.After(5 * time.Second) - - for !c.Connected { - select { - case pkt := <-c.Conn.PacketChan(): - if err := c.handlePacket(pkt); err != nil { - log.Printf("Error handling packet: %v", err) - } - case <-timeout: - return fmt.Errorf("connection timed out") - } - } - - log.Println("=== Connected! Now listening for server data... ===") - - // Send Ping every 3 seconds + // Listen Loop + pktChan := c.Conn.PacketChan() ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() - // KeepAlive Loop for { select { - case pkt := <-c.Conn.PacketChan(): + case pkt := <-pktChan: if err := c.handlePacket(pkt); err != nil { log.Printf("Error handling packet: %v", err) } case <-ticker.C: - // Send Ping - c.PacketIDCounterC2S++ ping := protocol.NewPacket(protocol.PacketTypePing, nil) - ping.Header.PacketID = c.PacketIDCounterC2S - ping.Header.ClientID = c.ClientID // Should be assigned by server usually, but we use 0 or what? - - // Encrypt Ping (if past handshake) - // For now, assuming unencrypted ping is ignored or we need to encrypt it if in full session - // Protocol says: "Everything is encrypted" - // Using correct keys... - - // Actually handlePacket sends PONG. We need to Initiate PING? - // Simplified: Just printing "Ping" for now, or just wait for server to Ping us. - // The server usually pings. We must reply Pong. - // BUT if we don't send anything, we might time out. - // Let's rely on Server Pings for now, but remove the 5s exit timeout. - } - } -} - -func (c *Client) handlePacket(pkt *protocol.Packet) error { - log.Printf("Received Packet: ID=%d, Type=%v, Len=%d", pkt.Header.PacketID, pkt.Header.PacketType(), len(pkt.Data)) - - switch pkt.Header.PacketType() { - case protocol.PacketTypeInit1: - return c.handleInit(pkt) - case protocol.PacketTypeCommand: - // Send ACK - // Ack Data: PacketID of the packet we're acknowledging (2 bytes) - ackData := make([]byte, 2) - binary.BigEndian.PutUint16(ackData, pkt.Header.PacketID) - - ack := protocol.NewPacket(protocol.PacketTypeAck, ackData) - - // ACK header PacketID should match the packet being acknowledged - ack.Header.PacketID = pkt.Header.PacketID - - // ACKs for Command packets during handshake are encrypted with HandshakeKey - key := protocol.HandshakeKey - nonce := protocol.HandshakeNonce - - // Meta for Client->Server: PID(2) + CID(2) + PT(1) = 5 bytes - meta := make([]byte, 5) - binary.BigEndian.PutUint16(meta[0:2], ack.Header.PacketID) - binary.BigEndian.PutUint16(meta[2:4], ack.Header.ClientID) // ClientID (usually 0 during handshake) - meta[4] = ack.Header.Type - - encData, mac, _ := protocol.EncryptEAX(key, nonce, meta, ack.Data) - ack.Data = encData - copy(ack.Header.MAC[:], mac) - log.Printf("Sending ACK for PacketID %d", pkt.Header.PacketID) - - c.Conn.SendPacket(ack) - - return c.handleCommand(pkt) - case protocol.PacketTypeVoice: - c.handleVoice(pkt) - case protocol.PacketTypePing: - // Respond with Pong - pong := protocol.NewPacket(protocol.PacketTypePong, nil) - pong.Header.PacketID = pkt.Header.PacketID // Acknowledgement - pong.Header.MAC = pkt.Header.MAC // TODO: calculate real mac - c.Conn.SendPacket(pong) - case protocol.PacketTypeAck: - // Server acknowledged our packet - ACKs are encrypted - // Decrypt with HandshakeKey - key := protocol.HandshakeKey - nonce := protocol.HandshakeNonce - - meta := make([]byte, 3) // Server->Client is 3 bytes - binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) - meta[2] = pkt.Header.Type - - data, err := protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) - if err != nil { - log.Printf("ACK decryption failed: %v", err) - return nil - } - - ackPId := uint16(0) - if len(data) >= 2 { - ackPId = binary.BigEndian.Uint16(data[0:2]) - } - log.Printf("Received ACK for PacketID %d", ackPId) - - // If ACK is for clientek (PID=1), proceed with clientinit - if ackPId == 1 && c.Handshake != nil && c.Handshake.Step == 5 { - log.Println("clientek acknowledged! Sending clientinit...") - c.Handshake.Step = 6 - return c.sendClientInit() - } - // If ACK is for clientinit (PID=2), we're connected! - if ackPId == 2 && c.Handshake != nil && c.Handshake.Step == 6 { - log.Println("clientinit acknowledged! Connection established!") - c.Connected = true - } - } - return nil -} - -func (c *Client) handleInit(pkt *protocol.Packet) error { - // Determine step based on packet content or local state - // Simple state machine - if c.Handshake.Step == 0 { - if err := c.Handshake.HandlePacket1(pkt); err != nil { - return err - } - log.Println("Handshake Step 1 Completed. Sending Step 2...") - return c.Handshake.SendPacket2() - } else if c.Handshake.Step == 1 { - // Wait, step 1 is processed, we sent step 2. - // We expect Step 3. - if pkt.Data[0] == 0x03 { - if err := c.Handshake.HandlePacket3(pkt); err != nil { - return err - } - log.Println("Handshake Step 3 Completed. Sending Step 4 (Puzzle Solution)...") - // Send Packet 4 (Not fully implemented in this snippet due to puzzle complexity) - // c.Handshake.SendPacket4() - } - } - return nil -} - -func (c *Client) handleCommand(pkt *protocol.Packet) error { - // Check if Encrypted - // PacketTypeCommand is usually encrypted. - // Flag check? The flag is in the Header (e.g. Unencrypted flag). - // If Unencrypted flag is SET, it's cleartext. - // Spec: "Command ... Encrypted: ✓". So Unencrypted flag is CLEARED. - - // Decrypt if necessary - var data []byte - var err error - - if pkt.Header.FlagUnencrypted() { - data = pkt.Data - } else { - var key, nonce []byte - decrypted := false - - // 1. Try SharedSecret if available - if c.Handshake != nil && c.Handshake.Step >= 6 && len(c.Handshake.SharedIV) > 0 { - // Use SharedSecret-based encryption - crypto := &protocol.CryptoState{ - SharedIV: c.Handshake.SharedIV, - SharedMac: c.Handshake.SharedMac, - GenerationID: 0, - } - // Server->Client = false - key, nonce = crypto.GenerateKeyNonce(&pkt.Header, false) - - // AAD for Server->Client: PacketID (2) + Type|Flags (1) - meta := make([]byte, 3) - binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) - meta[2] = pkt.Header.Type // Type includes Flags - - data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) - if err == nil { - decrypted = true - } else { - log.Printf("SharedSecret decrypt failed (PID=%d): %v. Trying HandshakeKey...", pkt.Header.PacketID, err) - } - } - - // 2. Fallback to HandshakeKey - if !decrypted { - key = protocol.HandshakeKey[:] - nonce = protocol.HandshakeNonce[:] - - // AAD matching KeyNonce derivation context? - // HandshakeKey usage usually has same AAD requirements? - meta := make([]byte, 3) - binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) - meta[2] = pkt.Header.Type // Type includes Flags - - data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) - if err != nil { - log.Printf("All decryption attempts failed for PID=%d: %v", pkt.Header.PacketID, err) - return fmt.Errorf("decryption failed: %v", err) - } - } - } - // On first encrypted command set Connected = true (Fallback if ACK missed) - if !c.Connected && pkt.Header.PacketID > 2 { - c.Connected = true - } - - // Fragment reassembly logic: - // - First fragment: Fragmented=true, optionally Compressed=true -> start buffer - // - Middle fragments: Fragmented=false, Compressed=false -> append to buffer - // - Last fragment: Fragmented=true -> append and process - isFragmented := pkt.Header.FlagFragmented() - - if isFragmented && !c.Fragmenting { - // First fragment - start collecting - c.Fragmenting = true - c.FragmentBuffer = make([]byte, 0, 4096) - c.FragmentBuffer = append(c.FragmentBuffer, data...) - c.FragmentStartPktID = pkt.Header.PacketID - c.FragmentCompressed = pkt.Header.FlagCompressed() - log.Printf("Fragment start (PID=%d, Compressed=%v, Len=%d)", pkt.Header.PacketID, c.FragmentCompressed, len(data)) - return nil // Wait for more fragments - } else if c.Fragmenting && !isFragmented { - // Middle fragment - append - c.FragmentBuffer = append(c.FragmentBuffer, data...) - log.Printf("Fragment continue (PID=%d, TotalLen=%d)", pkt.Header.PacketID, len(c.FragmentBuffer)) - return nil // Wait for more fragments - } else if c.Fragmenting && isFragmented { - // Last fragment - complete reassembly - c.FragmentBuffer = append(c.FragmentBuffer, data...) - log.Printf("Fragment end (PID=%d, TotalLen=%d)", pkt.Header.PacketID, len(c.FragmentBuffer)) - data = c.FragmentBuffer - - // Decompress if first fragment was compressed - if c.FragmentCompressed { - decompressed, err := quicklz.Decompress(data) - if err != nil { - log.Printf("QuickLZ decompression of fragmented data failed: %v", err) - // Fallback to raw data - } else { - log.Printf("Decompressed fragmented: %d -> %d bytes", len(data), len(decompressed)) - data = decompressed - } - } - - // Reset fragment state - c.Fragmenting = false - c.FragmentBuffer = nil - } else { - // Non-fragmented packet - decompress if needed - if pkt.Header.FlagCompressed() { - decompressed, err := quicklz.Decompress(data) - if err != nil { - log.Printf("QuickLZ decompression failed: %v (falling back to raw)", err) - // Fallback to raw data - might not be compressed despite flag - } else { - log.Printf("Decompressed: %d -> %d bytes", len(data), len(decompressed)) - data = decompressed - } - } - } - - cmdStr := string(data) - - // Debug: Log packet flags and raw command preview - log.Printf("Debug Packet: Compressed=%v, Fragmented=%v, RawLen=%d, Preview=%q", - pkt.Header.FlagCompressed(), pkt.Header.FlagFragmented(), len(data), - func() string { - if len(cmdStr) > 100 { - return cmdStr[:100] - } - return cmdStr - }()) - - // Fix Garbage Headers (TS3 often sends binary garbage before command) - // Scan for first valid lower case [a-z] char (Most commands are lowercase) - validStart := strings.IndexFunc(cmdStr, func(r rune) bool { - return (r >= 'a' && r <= 'z') - }) - - if validStart > 0 && validStart < 50 { - cmdStr = cmdStr[validStart:] - } - - log.Printf("Command: %s", cmdStr) - - // Parse Command - cmd, args := protocol.ParseCommand([]byte(cmdStr)) - - switch cmd { - case "initivexpand2": - err := c.Handshake.ProcessInitivexpand2(args) - if err != nil { - log.Printf("Error processing initivexpand2: %v", err) - } - case "initserver": - // Server sends this after clientinit - contains our clientID - if cid, ok := args["aclid"]; ok { - var id uint64 - fmt.Sscanf(cid, "%d", &id) - c.ClientID = uint16(id) - log.Printf("Assigned ClientID: %d", c.ClientID) - } - if name, ok := args["virtualserver_name"]; ok { - log.Printf("Server Name: %s", protocol.Unescape(name)) - } - case "channellist": - // Parse channel info - ch := &Channel{} - if cid, ok := args["cid"]; ok { - fmt.Sscanf(cid, "%d", &ch.ID) - } - if pid, ok := args["cpid"]; ok { - fmt.Sscanf(pid, "%d", &ch.ParentID) - } - if name, ok := args["channel_name"]; ok { - ch.Name = protocol.Unescape(name) - } - if order, ok := args["channel_order"]; ok { - fmt.Sscanf(order, "%d", &ch.Order) - } - c.Channels[ch.ID] = ch - log.Printf("Channel: [%d] NameRaw=%q Order=%d Args=%v", ch.ID, ch.Name, ch.Order, args) - case "channellistfinished": - log.Printf("=== Channel List Complete (%d channels) ===", len(c.Channels)) - var targetChan *Channel - for _, ch := range c.Channels { - log.Printf(" - [%d] %s (parent=%d)", ch.ID, ch.Name, ch.ParentID) - if ch.Name == "Test" { - targetChan = ch - } - } - - if targetChan == nil { - if ch, ok := c.Channels[2]; ok { - log.Printf("Name parsing failed. Defaulting to Channel 2 as 'Test'.") - targetChan = ch - } - } - - if targetChan != nil { - log.Printf("Found target channel 'Test' (ID=%d). Joining...", targetChan.ID) - - if c.ClientID == 0 { - log.Println("ERROR: ClientID is 0. Cannot join channel. 'initserver' missing?") - return nil - } - - // clientmove clid={clid} cid={cid} cpw= - cmd := fmt.Sprintf("clientmove clid=%d cid=%d cpw=", c.ClientID, targetChan.ID) - - pkt := protocol.NewPacket(protocol.PacketTypeCommand, []byte(cmd)) - - // Set NewProtocol flag (required for all commands) BEFORE computing meta - pkt.Header.Type |= protocol.PacketFlagNewProtocol - pkt.Header.PacketID = c.PacketIDCounterC2S + 1 - pkt.Header.ClientID = c.ClientID c.PacketIDCounterC2S++ + ping.Header.PacketID = c.PacketIDCounterC2S + ping.Header.ClientID = c.ClientID + // Must NOT have NewProtocol (0x20) flag for Pings/Pongs + ping.Header.Type = uint8(protocol.PacketTypePing) | protocol.PacketFlagUnencrypted - // Meta for Client->Server: PID(2) + CID(2) + PT(1) = 5 bytes - meta := make([]byte, 5) - binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) - binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID) - meta[4] = pkt.Header.Type // Now includes NewProtocol flag - - crypto := &protocol.CryptoState{ - SharedIV: c.Handshake.SharedIV, - SharedMac: c.Handshake.SharedMac, - GenerationID: 0, - } - k, n := crypto.GenerateKeyNonce(&pkt.Header, true) - - encData, mac, _ := protocol.EncryptEAX(k, n, meta, pkt.Data) - pkt.Data = encData - copy(pkt.Header.MAC[:], mac) - - log.Printf("Sending clientmove command: clid=%d cid=%d (PID=%d)", c.ClientID, targetChan.ID, pkt.Header.PacketID) - c.Conn.SendPacket(pkt) - } - case "notifycliententerview": - // A client entered the server - nick := "" - if n, ok := args["client_nickname"]; ok { - nick = protocol.Unescape(n) - log.Printf("Client entered: %s", nick) - - // If this matches our nickname, store the ClientID (Fallback if initserver missed) - if nick == c.Nickname && c.ClientID == 0 { - if clidStr, ok := args["clid"]; ok { - var id uint64 - fmt.Sscanf(clidStr, "%d", &id) - c.ClientID = uint16(id) - log.Printf("Identified Self via notifycliententerview! ClientID: %d", c.ClientID) + // Use SharedMac if available, otherwise zeros (as per ts3j InitPacketTransformation) + if c.Handshake != nil && len(c.Handshake.SharedMac) > 0 { + copy(ping.Header.MAC[:], c.Handshake.SharedMac) + } else { + // Initialize Header.MAC with zeros + for i := 0; i < 8; i++ { + ping.Header.MAC[i] = 0 } } - } - case "notifytextmessage": - if msg, ok := args["msg"]; ok { - log.Printf("Text Message: %s", protocol.Unescape(msg)) - } - case "notifychannelgrouplist": - // Ignore for now - case "notifyservergrouplist": - // Ignore for now - case "notifyclientneededpermissions": - // Ignore for now - case "notifyclientleftview": - if nick, ok := args["client_nickname"]; ok { - log.Printf("Client left: %s", protocol.Unescape(nick)) - } - case "notifyconnectioninfo": - // Ignore - case "badges": - // Server badges info - case "notifyclientchatcomposing": - if nick, ok := args["client_nickname"]; ok { - // This often comes as clid, need to lookup in future - log.Printf("Client typing: %s", protocol.Unescape(nick)) - } - case "notifyclientmoved": - if nick, ok := args["client_nickname"]; ok { - log.Printf("Client moved: %s", protocol.Unescape(nick)) - } - case "error": - if id, ok := args["id"]; ok && id == "522" { - log.Println("WARNING: Server rejected client version (Error 522). Ignoring as requested.") - // We pretend we are connected? - // The server might not send further data, but we won't crash. - c.Connected = true - } else { - log.Printf("Server Error: %v", args) - } - default: - // Handle prefixes for weirdly updated commands - if strings.HasPrefix(cmd, "badges") { - // ignore badges garbage - log.Println("Received Badges (Ignored)") - return nil - } - // Fuzzy match for corrupted notifycliententerview - if strings.HasPrefix(cmd, "notifyclient") { - // Attempt to process it anyway - nick := "" - if n, ok := args["client_nickname"]; ok { - nick = protocol.Unescape(n) - log.Printf("Fuzzy Notify Client Entered: %s", nick) - if nick == c.Nickname && c.ClientID == 0 { - if clidStr, ok := args["clid"]; ok { - var id uint64 - fmt.Sscanf(clidStr, "%d", &id) - c.ClientID = uint16(id) - log.Printf("Identified Self via Fuzzy Notify! ClientID: %d", c.ClientID) - } - } - } - return nil - } - // Log unknown commands for debugging - log.Printf("Unhandled command: %s Args: %v", cmd, args) + log.Printf("Sending KeepAlive Ping (PID=%d)", ping.Header.PacketID) + c.Conn.SendPacket(ping) + } } - - return nil -} - -// Helper to encrypt/decrypt based on state -func (c *Client) getCryptoState() (key, nonce, mac []byte, isHandshake bool) { - if c.Handshake != nil && len(c.Handshake.SharedSecret) > 0 { - // Use Derived Keys - // But we need to Generate Key/Nonce per packet! - // This logic belongs in the Packet Encode/Decode flow or a higher level wrapper? - return nil, nil, c.Handshake.SharedMac, false - } - return protocol.HandshakeKey, protocol.HandshakeNonce, protocol.HandshakeMac[:], true -} - -// Update encryption in Send/Receive -// Packet handling needs to know WHICH key to use. -// Simple rule: -// - Init1 (Type 8): Handshake Keys (Unencrypted payload, but MAC is HandshakeMac) -// - Command (Type 2): Encrypted. -// - CommandLow (Type 3): Encrypted. -// - Voice (Type 0): Encrypted. -// - Ping/Pong: Encrypted. -// - Ack: Encrypted. - -// IF c.Handshake.SharedSecret is set, we SHOULD use it for Commands? -// "The crypto handshake is now completed. The normal encryption scheme ... is from now on used." -// This starts AFTER clientek? Or WITH clientek? "clientek already has the packet id 1" - -func (c *Client) handleVoice(pkt *protocol.Packet) { - // Parse Voice Header (Server -> Client) - // VID(2) + CID(2) + Codec(1) + Data - if len(pkt.Data) < 5 { - return - } - - vid := binary.BigEndian.Uint16(pkt.Data[0:2]) - // cid := binary.BigEndian.Uint16(pkt.Data[2:4]) // Talking client ID (not needed for echo) - codec := pkt.Data[4] - voiceData := pkt.Data[5:] - - log.Printf("Voice Packet received. VID=%d, Codec=%d, Size=%d", vid, codec, len(voiceData)) - - // Build echo packet (Client -> Server) - // Format: VID(2) + Codec(1) + Data - echoData := make([]byte, 2+1+len(voiceData)) - binary.BigEndian.PutUint16(echoData[0:2], vid) - echoData[2] = codec - copy(echoData[3:], voiceData) - - echoPkt := protocol.NewPacket(protocol.PacketTypeVoice, echoData) - echoPkt.Header.PacketID = pkt.Header.PacketID // Use same ID for voice - echoPkt.Header.ClientID = c.ClientID - - // Encrypt voice packet with SharedSecret - if c.Handshake != nil && len(c.Handshake.SharedIV) > 0 { - crypto := &protocol.CryptoState{ - SharedIV: c.Handshake.SharedIV, - SharedMac: c.Handshake.SharedMac, - GenerationID: 0, - } - key, nonce := crypto.GenerateKeyNonce(&echoPkt.Header, true) - - // Meta for Client->Server: PID(2) + CID(2) + PT(1) - meta := make([]byte, 5) - binary.BigEndian.PutUint16(meta[0:2], echoPkt.Header.PacketID) - binary.BigEndian.PutUint16(meta[2:4], echoPkt.Header.ClientID) - meta[4] = echoPkt.Header.Type - - encData, mac, err := protocol.EncryptEAX(key, nonce, meta, echoPkt.Data) - if err != nil { - log.Printf("Voice encryption failed: %v", err) - return - } - echoPkt.Data = encData - copy(echoPkt.Header.MAC[:], mac) - } else { - // If no encryption keys, use SharedMac - echoPkt.Header.MAC = protocol.HandshakeMac - } - - c.Conn.SendPacket(echoPkt) -} - -func (c *Client) sendClientInit() error { - // Build clientinit command - // Build clientinit command using TeamSpeak 3.6.2 credentials - params := map[string]string{ - "client_nickname": c.Nickname, - "client_version": "3.6.2 [Build: 1690976575]", - "client_platform": "Windows", - "client_input_hardware": "1", - "client_output_hardware": "1", - "client_default_channel": "", - "client_default_channel_password": "", - "client_server_password": "", - "client_meta_data": "", - "client_version_sign": "OyuLO/1bVJtBsXLRWzfGVhNaQd7B9D4QTolZm14DM1uCbSXVvqX3Ssym3sLi/PcvOl+SAUlX6NwBPOsQdwOGDw==", - "client_key_offset": fmt.Sprintf("%d", c.Handshake.IdentityOffset), - "client_nickname_phonetic": "", - "client_default_token": "", - "hwid": "1234567890", - } - - // Construct command string manually to ensure key correctness - var buf bytes.Buffer - buf.WriteString("clientinit") - for k, v := range params { - buf.WriteString(" ") - buf.WriteString(k) - buf.WriteString("=") - buf.WriteString(protocol.Escape(v)) - } - cmd := buf.String() - - pkt := protocol.NewPacket(protocol.PacketTypeCommand, []byte(cmd)) - pkt.Header.PacketID = 2 // After clientek (1) - pkt.Header.Type |= protocol.PacketFlagNewProtocol - - // After clientek, use SharedSecret encryption (Now that we fixed derivation logic) - crypto := &protocol.CryptoState{ - SharedIV: c.Handshake.SharedIV, - SharedMac: c.Handshake.SharedMac, - GenerationID: 0, - } - // Client->Server = true - key, nonce := crypto.GenerateKeyNonce(&pkt.Header, true) - - // AAD must match the header structure exactly (excluding MAC) - // Client Header: PacketID (2) + ClientID (2) + Type|Flags (1) - meta := make([]byte, 5) - binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) - binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID) - - // Byte 4 is Type (which includes Flags) - meta[4] = pkt.Header.Type - - encData, mac, err := protocol.EncryptEAX(key, nonce, meta, pkt.Data) - if err != nil { - return err - } - - pkt.Data = encData - copy(pkt.Header.MAC[:], mac) - - log.Println("Sending clientinit (Packet 2) [Encrypted with SharedSecret]...") - c.PacketIDCounterC2S = 2 // Update counter after clientinit - return c.Conn.SendPacket(pkt) } diff --git a/internal/client/commands.go b/internal/client/commands.go new file mode 100644 index 0000000..a53525a --- /dev/null +++ b/internal/client/commands.go @@ -0,0 +1,327 @@ +package client + +import ( + "encoding/binary" + "fmt" + "log" + "strings" + + "go-ts/pkg/protocol" + + "github.com/dgryski/go-quicklz" +) + +func (c *Client) handleCommand(pkt *protocol.Packet) error { + // Check if Encrypted + // PacketTypeCommand is usually encrypted. + // Flag check? The flag is in the Header (e.g. Unencrypted flag). + // If Unencrypted flag is SET, it's cleartext. + // Spec: "Command ... Encrypted: ✓". So Unencrypted flag is CLEARED. + + // Decrypt if necessary + var data []byte + var err error + + if pkt.Header.FlagUnencrypted() { + data = pkt.Data + } else { + var key, nonce []byte + decrypted := false + + // 1. Try SharedSecret if available + if c.Handshake != nil && c.Handshake.Step >= 6 && len(c.Handshake.SharedIV) > 0 { + // Use SharedSecret-based encryption + crypto := &protocol.CryptoState{ + SharedIV: c.Handshake.SharedIV, + SharedMac: c.Handshake.SharedMac, + GenerationID: 0, + } + // Server->Client = false + key, nonce = crypto.GenerateKeyNonce(&pkt.Header, false) + + // AAD for Server->Client: PacketID (2) + Type|Flags (1) + meta := make([]byte, 3) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + meta[2] = pkt.Header.Type // Type includes Flags + + data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) + if err == nil { + decrypted = true + } else { + log.Printf("SharedSecret decrypt failed (PID=%d): %v. Trying HandshakeKey...", pkt.Header.PacketID, err) + } + } + + // 2. Fallback to HandshakeKey + if !decrypted { + key = protocol.HandshakeKey[:] + nonce = protocol.HandshakeNonce[:] + + // AAD matching KeyNonce derivation context? + // HandshakeKey usage usually has same AAD requirements? + meta := make([]byte, 3) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + meta[2] = pkt.Header.Type // Type includes Flags + + data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) + if err != nil { + log.Printf("All decryption attempts failed for PID=%d: %v", pkt.Header.PacketID, err) + return fmt.Errorf("decryption failed: %v", err) + } + } + } + // On first encrypted command set Connected = true (Fallback if ACK missed) + if !c.Connected && pkt.Header.PacketID > 2 { + c.Connected = true + } + + // Fragment reassembly logic: + // - First fragment: Fragmented=true, optionally Compressed=true -> start buffer + // - Middle fragments: Fragmented=false, Compressed=false -> append to buffer + // - Last fragment: Fragmented=true -> append and process + isFragmented := pkt.Header.FlagFragmented() + + if isFragmented && !c.Fragmenting { + // First fragment - start collecting + c.Fragmenting = true + c.FragmentBuffer = make([]byte, 0, 4096) + c.FragmentBuffer = append(c.FragmentBuffer, data...) + c.FragmentStartPktID = pkt.Header.PacketID + c.FragmentCompressed = pkt.Header.FlagCompressed() + log.Printf("Fragment start (PID=%d, Compressed=%v, Len=%d)", pkt.Header.PacketID, c.FragmentCompressed, len(data)) + return nil // Wait for more fragments + } else if c.Fragmenting && !isFragmented { + // Middle fragment - append + c.FragmentBuffer = append(c.FragmentBuffer, data...) + log.Printf("Fragment continue (PID=%d, TotalLen=%d)", pkt.Header.PacketID, len(c.FragmentBuffer)) + return nil // Wait for more fragments + } else if c.Fragmenting && isFragmented { + // Last fragment - complete reassembly + c.FragmentBuffer = append(c.FragmentBuffer, data...) + log.Printf("Fragment end (PID=%d, TotalLen=%d)", pkt.Header.PacketID, len(c.FragmentBuffer)) + data = c.FragmentBuffer + + // Decompress if first fragment was compressed + if c.FragmentCompressed { + decompressed, err := quicklz.Decompress(data) + if err != nil { + log.Printf("QuickLZ decompression of fragmented data failed: %v", err) + // Fallback to raw data + } else { + log.Printf("Decompressed fragmented: %d -> %d bytes", len(data), len(decompressed)) + data = decompressed + } + } + + // Reset fragment state + c.Fragmenting = false + c.FragmentBuffer = nil + } else { + // Non-fragmented packet - decompress if needed + if pkt.Header.FlagCompressed() { + decompressed, err := quicklz.Decompress(data) + if err != nil { + log.Printf("QuickLZ decompression failed: %v (falling back to raw)", err) + // Fallback to raw data - might not be compressed despite flag + } else { + log.Printf("Decompressed: %d -> %d bytes", len(data), len(decompressed)) + data = decompressed + } + } + } + + cmdStr := string(data) + + // Debug: Log packet flags and raw command preview + log.Printf("Debug Packet: Compressed=%v, Fragmented=%v, RawLen=%d, Preview=%q", + pkt.Header.FlagCompressed(), pkt.Header.FlagFragmented(), len(data), + func() string { + if len(cmdStr) > 100 { + return cmdStr[:100] + } + return cmdStr + }()) + + // Fix Garbage Headers (TS3 often sends binary garbage before command) + // Scan for first valid lower case [a-z] char (Most commands are lowercase) + validStart := strings.IndexFunc(cmdStr, func(r rune) bool { + return (r >= 'a' && r <= 'z') + }) + + if validStart > 0 && validStart < 50 { + cmdStr = cmdStr[validStart:] + } + + log.Printf("Command: %s", cmdStr) + + // Parse Command + cmd, args := protocol.ParseCommand([]byte(cmdStr)) + + switch cmd { + case "initivexpand2": + err := c.Handshake.ProcessInitivexpand2(args) + if err != nil { + log.Printf("Error processing initivexpand2: %v", err) + } + case "initserver": + // Server sends this after clientinit - contains our clientID + if cid, ok := args["aclid"]; ok { + var id uint64 + fmt.Sscanf(cid, "%d", &id) + c.ClientID = uint16(id) + log.Printf("Assigned ClientID: %d", c.ClientID) + } + if name, ok := args["virtualserver_name"]; ok { + log.Printf("Server Name: %s", protocol.Unescape(name)) + } + case "channellist": + // Parse channel info + ch := &Channel{} + if cid, ok := args["cid"]; ok { + fmt.Sscanf(cid, "%d", &ch.ID) + } + if pid, ok := args["cpid"]; ok { + fmt.Sscanf(pid, "%d", &ch.ParentID) + } + if name, ok := args["channel_name"]; ok { + ch.Name = protocol.Unescape(name) + } + if order, ok := args["channel_order"]; ok { + fmt.Sscanf(order, "%d", &ch.Order) + } + c.Channels[ch.ID] = ch + log.Printf("Channel: [%d] NameRaw=%q Order=%d Args=%v", ch.ID, ch.Name, ch.Order, args) + case "channellistfinished": + log.Printf("=== Channel List Complete (%d channels) ===", len(c.Channels)) + var targetChan *Channel + for _, ch := range c.Channels { + log.Printf(" - [%d] %s (parent=%d)", ch.ID, ch.Name, ch.ParentID) + if ch.Name == "Test" { + targetChan = ch + } + } + + if targetChan == nil { + if ch, ok := c.Channels[2]; ok { + log.Printf("Name parsing failed. Defaulting to Channel 2 as 'Test'.") + targetChan = ch + } + } + + if targetChan != nil { + log.Printf("Found target channel 'Test' (ID=%d). Joining...", targetChan.ID) + + if c.ClientID == 0 { + log.Println("ERROR: ClientID is 0. Cannot join channel. 'initserver' missing?") + return nil + } + + moveCmd := protocol.NewCommand("clientmove") + moveCmd.AddParam("clid", fmt.Sprintf("%d", c.ClientID)) + moveCmd.AddParam("cid", fmt.Sprintf("%d", targetChan.ID)) + moveCmd.AddParam("cpw", "") + + return c.SendCommand(moveCmd) + } + case "notifycliententerview": + // A client entered the server + nick := "" + if n, ok := args["client_nickname"]; ok { + nick = protocol.Unescape(n) + log.Printf("Client entered: %s", nick) + } + case "notifytextmessage": + // targetmode: 1=Private, 2=Channel, 3=Server + msg := "" + invoker := "Unknown" + if m, ok := args["msg"]; ok { + msg = protocol.Unescape(m) + } + if name, ok := args["invokername"]; ok { + invoker = protocol.Unescape(name) + } + + targetMode := "Unknown" + if tm, ok := args["targetmode"]; ok { + switch tm { + case "1": + targetMode = "Private" + case "2": + targetMode = "Channel" + case "3": + targetMode = "Server" + } + } + + log.Printf("[Chat][%s] %s: %s", targetMode, invoker, msg) + + case "notifyclientchatcomposing": + // Someone is typing + // We only get clid, need to map to name if possible, or just log clid + clid := "Unknown" + if id, ok := args["clid"]; ok { + clid = id + } + log.Printf("Client %s is typing...", clid) + + case "notifyclientmoved": + // Client moved to another channel + clid := args["clid"] + ctid := args["ctid"] + // reasonid: 0=switched, 1=moved, 2=timeout, 3=kick, 4=unknown + log.Printf("Client %s moved to Channel %s", clid, ctid) + + case "notifyclientchannelgroupchanged": + // Client channel group changed + // invokerid=0 invokername=Server cgid=8 cid=1 clid=3 cgi=1 + invoker := "Unknown" + if name, ok := args["invokername"]; ok { + invoker = protocol.Unescape(name) + } + log.Printf("Client %s channel group changed to %s in Channel %s by %s", + args["clid"], args["cgid"], args["cid"], invoker) + + case "notifyconnectioninforequest": + // Server asking for connection info. We MUST reply to update Ping in UI and avoid timeout. + log.Println("Server requested connection info. sending 'setconnectioninfo'...") + + cmd := protocol.NewCommand("setconnectioninfo") + cmd.AddParam("connection_ping", "50") + cmd.AddParam("connection_ping_deviation", "5") + + // Detailed stats for each kind as seen in ts3j (KEEPALIVE, SPEECH, CONTROL) + kinds := []string{"keepalive", "speech", "control"} + for _, k := range kinds { + cmd.AddParam("connection_packets_sent_"+k, "500") + cmd.AddParam("connection_packets_received_"+k, "500") + cmd.AddParam("connection_bytes_sent_"+k, "25000") + cmd.AddParam("connection_bytes_received_"+k, "25000") + cmd.AddParam("connection_bandwidth_sent_last_second_"+k, "200") + cmd.AddParam("connection_bandwidth_received_last_second_"+k, "200") + cmd.AddParam("connection_bandwidth_sent_last_minute_"+k, "200") + cmd.AddParam("connection_bandwidth_received_last_minute_"+k, "200") + cmd.AddParam("connection_server2client_packetloss_"+k, "0") + } + cmd.AddParam("connection_server2client_packetloss_total", "0") + + return c.SendCommand(cmd) + + case "notifyclientupdated": + // Client updated (e.g. muted/unmuted) + clid := args["clid"] + log.Printf("Client %s updated: %v", clid, args) + + case "error": + // Server reported an error + id := args["id"] + msg := protocol.Unescape(args["msg"]) + log.Printf("SERVER ERROR: ID=%s MSG=%s", id, msg) + + case "notifyservergrouplist", "notifychannelgrouplist", "notifyclientneededpermissions": + // Ignore verbose noisy setup commands + default: + log.Printf("Unhandled command: %s Args: %v", cmd, args) + } + + return nil +} diff --git a/internal/client/handshake.go b/internal/client/handshake.go index 663c2f8..6a295c9 100644 --- a/internal/client/handshake.go +++ b/internal/client/handshake.go @@ -290,7 +290,8 @@ func (h *HandshakeState) ProcessInitivexpand2(cmdArgs map[string]string) error { // SharedMac = SHA1(SharedIV)[0..8] macHash := sha1.Sum(h.SharedIV) - copy(h.SharedMac[:], macHash[0:8]) + h.SharedMac = make([]byte, 8) + copy(h.SharedMac, macHash[0:8]) log.Printf("Debug - SharedSecret (SHA512): %s", hex.EncodeToString(h.SharedSecret)) log.Printf("Debug - SharedIV: %s", hex.EncodeToString(h.SharedIV)) diff --git a/internal/client/packet.go b/internal/client/packet.go new file mode 100644 index 0000000..1be9318 --- /dev/null +++ b/internal/client/packet.go @@ -0,0 +1,172 @@ +package client + +import ( + "bytes" + "encoding/binary" + "log" + + "go-ts/pkg/protocol" +) + +func (c *Client) handlePacket(pkt *protocol.Packet) error { + log.Printf("Received Packet: ID=%d, Type=%v, Len=%d", pkt.Header.PacketID, pkt.Header.PacketType(), len(pkt.Data)) + + switch pkt.Header.PacketType() { + case protocol.PacketTypeInit1: + return c.handleInit(pkt) + case protocol.PacketTypeCommand: + // Send ACK + ackData := make([]byte, 2) + binary.BigEndian.PutUint16(ackData, pkt.Header.PacketID) + + ack := protocol.NewPacket(protocol.PacketTypeAck, ackData) + // Spec/ts3j: Header PID for ACK matches the packet being acknowledged + ack.Header.PacketID = pkt.Header.PacketID + ack.Header.ClientID = c.ClientID + // ACKs usually don't have NewProtocol flag set in Header byte + ack.Header.Type &= ^uint8(protocol.PacketFlagNewProtocol) + + // ACKs for Command packets after handshake must be encrypted + key := protocol.HandshakeKey + nonce := protocol.HandshakeNonce + + if c.Handshake != nil && c.Handshake.Step >= 6 && len(c.Handshake.SharedIV) > 0 { + crypto := &protocol.CryptoState{ + SharedIV: c.Handshake.SharedIV, + SharedMac: c.Handshake.SharedMac, + GenerationID: 0, + } + key, nonce = crypto.GenerateKeyNonce(&ack.Header, true) // Client->Server=true + } + + // Meta for Client->Server: PID(2) + CID(2) + PT(1) = 5 bytes + meta := make([]byte, 5) + binary.BigEndian.PutUint16(meta[0:2], ack.Header.PacketID) + binary.BigEndian.PutUint16(meta[2:4], ack.Header.ClientID) + meta[4] = ack.Header.Type + + encData, mac, _ := protocol.EncryptEAX(key, nonce, meta, ack.Data) + ack.Data = encData + copy(ack.Header.MAC[:], mac) + // log.Printf("Sending ACK for server Command PID=%d", pkt.Header.PacketID) + + c.Conn.SendPacket(ack) + + return c.handleCommand(pkt) + case protocol.PacketTypeVoice: + c.handleVoice(pkt) + case protocol.PacketTypePing: + // Respond with Pong + pong := protocol.NewPacket(protocol.PacketTypePong, nil) + // Spec/ts3j: Header PID for Pong matches the Ping ID + pong.Header.PacketID = pkt.Header.PacketID + pong.Header.ClientID = c.ClientID + // Must NOT have NewProtocol (0x20) flag for Pings/Pongs + pong.Header.Type = uint8(protocol.PacketTypePong) | protocol.PacketFlagUnencrypted + + // Use SharedMac if available, otherwise zeros + if c.Handshake != nil && len(c.Handshake.SharedMac) > 0 { + copy(pong.Header.MAC[:], c.Handshake.SharedMac) + } else { + for i := 0; i < 8; i++ { + pong.Header.MAC[i] = 0 + } + } + + // The body of the Pong must contain the PID of the Ping it's acknowledging + pong.Data = make([]byte, 2) + binary.BigEndian.PutUint16(pong.Data, pkt.Header.PacketID) + + log.Printf("Sending Pong (HeaderPID=%d) for Ping", pong.Header.PacketID) + c.Conn.SendPacket(pong) + case protocol.PacketTypePong: + // Server acknowledged our Ping + log.Printf("Received Pong for sequence %d", pkt.Header.PacketID) + case protocol.PacketTypeAck: + // Server acknowledged our packet + var data []byte + var err error + + if pkt.Header.FlagUnencrypted() { + data = pkt.Data + } else { + // ACKs are encrypted + key := protocol.HandshakeKey + nonce := protocol.HandshakeNonce + + if c.Handshake != nil && c.Handshake.Step >= 6 && len(c.Handshake.SharedIV) > 0 { + // Use SharedSecret + crypto := &protocol.CryptoState{ + SharedIV: c.Handshake.SharedIV, + SharedMac: c.Handshake.SharedMac, + GenerationID: 0, + } + key, nonce = crypto.GenerateKeyNonce(&pkt.Header, false) // Server->Client=false + } + + meta := make([]byte, 3) // Server->Client is 3 bytes + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + meta[2] = pkt.Header.Type + + data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) + if err != nil { + // Try fallback to HandshakeKey if SharedSecret failed + if !bytes.Equal(key, protocol.HandshakeKey[:]) { + log.Printf("ACK SharedSecret decrypt failed, trying HandshakeKey...") + key = protocol.HandshakeKey[:] + nonce = protocol.HandshakeNonce[:] + data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) + } + + if err != nil { + log.Printf("ACK decryption failed (PID=%d): %v", pkt.Header.PacketID, err) + return nil + } + } + } + + ackPId := uint16(0) + if len(data) >= 2 { + ackPId = binary.BigEndian.Uint16(data[0:2]) + } + log.Printf("Received ACK for PacketID %d (HeaderPID=%d)", ackPId, pkt.Header.PacketID) + + // If ACK is for clientek (PID=1), proceed with clientinit + if ackPId == 1 && c.Handshake != nil && c.Handshake.Step == 5 { + log.Println("clientek acknowledged! Sending clientinit...") + c.Handshake.Step = 6 + return c.sendClientInit() + } + // If ACK is for clientinit (PID=2), we're connected! + if ackPId == 2 && c.Handshake != nil && c.Handshake.Step == 6 { + log.Println("clientinit acknowledged! Connection established!") + c.Connected = true + } + } + return nil +} + +func (c *Client) handleInit(pkt *protocol.Packet) error { + // Determine step based on packet content or local state + // Simple state machine + if c.Handshake.Step == 0 { + if err := c.Handshake.HandlePacket1(pkt); err != nil { + return err + } + log.Println("Handshake Step 1 Completed. Sending Step 2...") + return c.Handshake.SendPacket2() + } else if c.Handshake.Step == 1 { + // Wait, step 1 is processed, we sent step 2. + // We expect Step 3. + if pkt.Data[0] == 0x03 { + if err := c.Handshake.HandlePacket3(pkt); err != nil { + return err + } + log.Println("Handshake Step 3 Completed. Sending Step 4 (Puzzle Solution)...") + if err := c.Handshake.SendPacket4(); err != nil { + return err + } + } + } + return nil +} diff --git a/internal/client/send.go b/internal/client/send.go new file mode 100644 index 0000000..61542eb --- /dev/null +++ b/internal/client/send.go @@ -0,0 +1,146 @@ +package client + +import ( + "bytes" + "encoding/binary" + "fmt" + "log" + + "go-ts/pkg/protocol" + + "github.com/dgryski/go-quicklz" +) + +// SendCommand sends a command, splitting it into fragments if it exceeds 500 bytes. +func (c *Client) SendCommand(cmd *protocol.Command) error { + return c.SendCommandString(cmd.Encode()) +} + +// SendCommandString sends a raw command string with fragmentation. +func (c *Client) SendCommandString(cmdStr string) error { + data := []byte(cmdStr) + maxPacketSize := 500 + maxBody := maxPacketSize - 13 // Header is 13 bytes for C->S (MAC 8, PID 2, TYPE 1, CID 2) + + pType := protocol.PacketTypeCommand + pFlags := uint8(0) + + // ts3j logic: If too large, try compressing + if len(data)+13 > maxPacketSize { + compressed := quicklz.Compress(data, 1) + if len(compressed)+13 < len(data)+13 { + data = compressed + pFlags |= protocol.PacketFlagCompressed + log.Printf("Compressed large command: %d -> %d bytes", len([]byte(cmdStr)), len(data)) + } + } + + // If still too large (or not compressible), fragment + if len(data)+13 > maxPacketSize { + log.Printf("Fragmenting large command (%d bytes) into %d packets", len(data), (len(data)/maxBody)+1) + + for i := 0; i < len(data); i += maxBody { + end := i + maxBody + if end > len(data) { + end = len(data) + } + + chunk := data[i:end] + chunkFlags := uint8(0) + + // First packet keeps COMPRESSED flag (if set) and gets FRAGMENTED + if i == 0 { + chunkFlags = pFlags | protocol.PacketFlagFragmented + } else if end == len(data) { + // Last packet gets FRAGMENTED + chunkFlags = protocol.PacketFlagFragmented + } else { + // Intermediate packets have NO flags (other than NewProtocol added in sendPacketInternal) + chunkFlags = 0 + } + + if err := c.sendPacketInternal(chunk, pType, chunkFlags); err != nil { + return err + } + } + return nil + } + + // Small enough to send in one go + return c.sendPacketInternal(data, pType, pFlags) +} + +// sendPacketInternal handles encryption and low-level header construction for C->S packets. +func (c *Client) sendPacketInternal(data []byte, pType protocol.PacketType, flags uint8) error { + pkt := protocol.NewPacket(pType, data) + c.PacketIDCounterC2S++ + pkt.Header.PacketID = c.PacketIDCounterC2S + pkt.Header.ClientID = c.ClientID + pkt.Header.Type |= protocol.PacketFlagNewProtocol | flags + + // Encryption + // Use SharedSecret if Step >= 6, else fallback to HandshakeKey + key := protocol.HandshakeKey[:] + nonce := protocol.HandshakeNonce[:] + + if c.Handshake != nil && c.Handshake.Step >= 6 && len(c.Handshake.SharedIV) > 0 { + crypto := &protocol.CryptoState{ + SharedIV: c.Handshake.SharedIV, + SharedMac: c.Handshake.SharedMac, + GenerationID: 0, + } + keyArr, nonceArr := crypto.GenerateKeyNonce(&pkt.Header, true) // Client->Server=true + key = keyArr + nonce = nonceArr + } + + // Meta for Client->Server: PID(2) + CID(2) + PT(1) = 5 bytes + meta := make([]byte, 5) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID) + meta[4] = pkt.Header.Type + + encData, mac, err := protocol.EncryptEAX(key, nonce, meta, pkt.Data) + if err != nil { + return fmt.Errorf("encryption failed: %w", err) + } + + pkt.Data = encData + copy(pkt.Header.MAC[:], mac) + + return c.Conn.SendPacket(pkt) +} + +func (c *Client) sendClientInit() error { + // Specialized send for clientinit because it needs PID 2 and uses Map params + params := map[string]string{ + "client_nickname": c.Nickname, + "client_version": "3.6.2 [Build: 1690976575]", + "client_platform": "Windows", + "client_input_muted": "0", + "client_output_muted": "0", + "client_outputonly_muted": "0", + "client_input_hardware": "1", + "client_output_hardware": "1", + "client_version_sign": "OyuLO/1bVJtBsXLRWzfGVhNaQd7B9D4QTolZm14DM1uCbSXVvqX3Ssym3sLi/PcvOl+SAUlX6NwBPOsQdwOGDw==", + "client_key_offset": fmt.Sprintf("%d", c.Handshake.IdentityOffset), + "hwid": "1234567890", + } + + var buf bytes.Buffer + buf.WriteString("clientinit") + for k, v := range params { + buf.WriteString(" ") + buf.WriteString(k) + if v != "" { + buf.WriteString("=") + buf.WriteString(protocol.Escape(v)) + } + } + + log.Println("Sending clientinit (Packet 2) [Encrypted]...") + + // Reset counter specifically for this sync point + c.PacketIDCounterC2S = 1 // Next will be 2 + return c.SendCommandString(buf.String()) +} diff --git a/internal/client/voice.go b/internal/client/voice.go new file mode 100644 index 0000000..54a39d5 --- /dev/null +++ b/internal/client/voice.go @@ -0,0 +1,67 @@ +package client + +import ( + "encoding/binary" + "log" + + "go-ts/pkg/protocol" +) + +func (c *Client) handleVoice(pkt *protocol.Packet) { + // Parse Voice Header (Server -> Client) + // VID(2) + CID(2) + Codec(1) + Data + if len(pkt.Data) < 5 { + return + } + + vid := binary.BigEndian.Uint16(pkt.Data[0:2]) + // cid := binary.BigEndian.Uint16(pkt.Data[2:4]) // Talking client ID (not needed for echo) + codec := pkt.Data[4] + voiceData := pkt.Data[5:] + + log.Printf("Voice Packet received. VID=%d, Codec=%d, Size=%d", vid, codec, len(voiceData)) + + // Build echo packet (Client -> Server) + // Format: VID(2) + Codec(1) + Data + echoData := make([]byte, 2+1+len(voiceData)) + binary.BigEndian.PutUint16(echoData[0:2], vid) + echoData[2] = codec + copy(echoData[3:], voiceData) + + echoPkt := protocol.NewPacket(protocol.PacketTypeVoice, echoData) + echoPkt.Header.PacketID = pkt.Header.PacketID // Use same ID for voice + echoPkt.Header.ClientID = c.ClientID + + // Encrypt voice packet with SharedSecret + if c.Handshake != nil && len(c.Handshake.SharedIV) > 0 { + crypto := &protocol.CryptoState{ + SharedIV: c.Handshake.SharedIV, + SharedMac: c.Handshake.SharedMac, + GenerationID: 0, + } + key, nonce := crypto.GenerateKeyNonce(&echoPkt.Header, true) + + // Meta for Client->Server: PID(2) + CID(2) + PT(1) + meta := make([]byte, 5) + binary.BigEndian.PutUint16(meta[0:2], echoPkt.Header.PacketID) + binary.BigEndian.PutUint16(meta[2:4], echoPkt.Header.ClientID) + meta[4] = echoPkt.Header.Type + + encData, mac, err := protocol.EncryptEAX(key, nonce, meta, echoPkt.Data) + if err != nil { + log.Printf("Voice encryption failed: %v", err) + return + } + echoPkt.Data = encData + copy(echoPkt.Header.MAC[:], mac) + } else { + // If no encryption keys, use SharedMac if available, otherwise HandshakeMac + if c.Handshake != nil && len(c.Handshake.SharedMac) > 0 { + copy(echoPkt.Header.MAC[:], c.Handshake.SharedMac) + } else { + echoPkt.Header.MAC = protocol.HandshakeMac + } + } + + c.Conn.SendPacket(echoPkt) +} diff --git a/pkg/protocol/commands.go b/pkg/protocol/commands.go index 893df65..ffe90af 100644 --- a/pkg/protocol/commands.go +++ b/pkg/protocol/commands.go @@ -57,3 +57,34 @@ func Escape(s string) string { ) return r.Replace(s) } + +// Command represents a TeamSpeak 3 command for building/encoding +type Command struct { + Name string + Params map[string]string +} + +func NewCommand(name string) *Command { + return &Command{ + Name: name, + Params: make(map[string]string), + } +} + +func (c *Command) AddParam(key, value string) { + c.Params[key] = value +} + +func (c *Command) Encode() string { + var sb strings.Builder + sb.WriteString(c.Name) + for k, v := range c.Params { + sb.WriteString(" ") + sb.WriteString(k) + if v != "" { + sb.WriteString("=") + sb.WriteString(Escape(v)) + } + } + return sb.String() +}