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 { ID uint64 ParentID uint64 Name string Order uint64 } type Client struct { Conn *transport.TS3Conn Handshake *HandshakeState Nickname string ClientID uint16 // Counters PacketIDCounterC2S uint16 // State Connected bool // Fragment reassembly FragmentBuffer []byte FragmentStartPktID uint16 FragmentCompressed bool Fragmenting bool // Server Data Channels map[uint64]*Channel } func NewClient(nickname string) *Client { return &Client{ Nickname: nickname, PacketIDCounterC2S: 1, Channels: make(map[uint64]*Channel), } } func (c *Client) Connect(address string) error { conn, err := transport.NewTS3Conn(address) if err != nil { return err } c.Conn = conn // Initialize handshake state hs, err := NewHandshakeState(c.Conn) if err != nil { return err } c.Handshake = hs // Improve Identity Security Level to 8 (Standard Requirement) c.Handshake.ImproveSecurityLevel(8) log.Println("Connected to UDP. Starting Handshake...") // Start Handshake Flow // Step 0 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 ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() // KeepAlive Loop for { select { case pkt := <-c.Conn.PacketChan(): 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++ // 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) } } } 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) } 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) }