Stabilize TeamSpeak connection: implement command compression, fragmentation, and fix MAC/flags
This commit is contained in:
@@ -1,17 +1,11 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-ts/pkg/protocol"
|
"go-ts/pkg/protocol"
|
||||||
"go-ts/pkg/transport"
|
"go-ts/pkg/transport"
|
||||||
|
|
||||||
"github.com/dgryski/go-quicklz"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
@@ -58,7 +52,9 @@ func (c *Client) Connect(address string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.Conn = conn
|
c.Conn = conn
|
||||||
// Initialize handshake state
|
log.Printf("Connected to UDP. Starting Handshake...")
|
||||||
|
|
||||||
|
// Initialize Handshake State
|
||||||
hs, err := NewHandshakeState(c.Conn)
|
hs, err := NewHandshakeState(c.Conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -68,630 +64,42 @@ func (c *Client) Connect(address string) error {
|
|||||||
// Improve Identity Security Level to 8 (Standard Requirement)
|
// Improve Identity Security Level to 8 (Standard Requirement)
|
||||||
c.Handshake.ImproveSecurityLevel(8)
|
c.Handshake.ImproveSecurityLevel(8)
|
||||||
|
|
||||||
log.Println("Connected to UDP. Starting Handshake...")
|
// Send Init1
|
||||||
|
|
||||||
// Start Handshake Flow
|
|
||||||
// Step 0
|
|
||||||
if err := c.Handshake.SendPacket0(); err != nil {
|
if err := c.Handshake.SendPacket0(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read Loop for Handshake
|
// Listen Loop
|
||||||
timeout := time.After(5 * time.Second)
|
pktChan := c.Conn.PacketChan()
|
||||||
|
|
||||||
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)
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
// KeepAlive Loop
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case pkt := <-c.Conn.PacketChan():
|
case pkt := <-pktChan:
|
||||||
if err := c.handlePacket(pkt); err != nil {
|
if err := c.handlePacket(pkt); err != nil {
|
||||||
log.Printf("Error handling packet: %v", err)
|
log.Printf("Error handling packet: %v", err)
|
||||||
}
|
}
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Send Ping
|
|
||||||
c.PacketIDCounterC2S++
|
|
||||||
ping := protocol.NewPacket(protocol.PacketTypePing, nil)
|
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++
|
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
|
// Use SharedMac if available, otherwise zeros (as per ts3j InitPacketTransformation)
|
||||||
meta := make([]byte, 5)
|
if c.Handshake != nil && len(c.Handshake.SharedMac) > 0 {
|
||||||
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
|
copy(ping.Header.MAC[:], c.Handshake.SharedMac)
|
||||||
binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID)
|
} else {
|
||||||
meta[4] = pkt.Header.Type // Now includes NewProtocol flag
|
// Initialize Header.MAC with zeros
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
crypto := &protocol.CryptoState{
|
ping.Header.MAC[i] = 0
|
||||||
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("Sending KeepAlive Ping (PID=%d)", ping.Header.PacketID)
|
||||||
log.Printf("Unhandled command: %s Args: %v", cmd, args)
|
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
327
internal/client/commands.go
Normal file
327
internal/client/commands.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -290,7 +290,8 @@ func (h *HandshakeState) ProcessInitivexpand2(cmdArgs map[string]string) error {
|
|||||||
|
|
||||||
// SharedMac = SHA1(SharedIV)[0..8]
|
// SharedMac = SHA1(SharedIV)[0..8]
|
||||||
macHash := sha1.Sum(h.SharedIV)
|
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 - SharedSecret (SHA512): %s", hex.EncodeToString(h.SharedSecret))
|
||||||
log.Printf("Debug - SharedIV: %s", hex.EncodeToString(h.SharedIV))
|
log.Printf("Debug - SharedIV: %s", hex.EncodeToString(h.SharedIV))
|
||||||
|
|||||||
172
internal/client/packet.go
Normal file
172
internal/client/packet.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
146
internal/client/send.go
Normal file
146
internal/client/send.go
Normal file
@@ -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())
|
||||||
|
}
|
||||||
67
internal/client/voice.go
Normal file
67
internal/client/voice.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -57,3 +57,34 @@ func Escape(s string) string {
|
|||||||
)
|
)
|
||||||
return r.Replace(s)
|
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()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user