Files
go-ts/cmd/proxy/main_design.go
Jose Luis Montañes Ojados 47b8173045 working
2026-01-15 16:49:16 +01:00

139 lines
4.8 KiB
Go

package main
import (
"flag"
"log"
"net"
"strings"
)
// TS3 Proxy to capture clientinit parameters
func main() {
var localAddr string
var serverAddr string
flag.StringVar(&localAddr, "listen", ":9988", "Local listen address")
flag.StringVar(&serverAddr, "server", "localhost:9987", "Real TeamSpeak server address")
flag.Parse()
// Resolve addresses
localUDP, err := net.ResolveUDPAddr("udp", localAddr)
if err != nil {
log.Fatalf("Invalid local address: %v", err)
}
serverUDP, err := net.ResolveUDPAddr("udp", serverAddr)
if err != nil {
log.Fatalf("Invalid server address: %v", err)
}
// Listen on local port
conn, err := net.ListenUDP("udp", localUDP)
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
defer conn.Close()
log.Printf("Proxy listening on %s, forwarding to %s", localAddr, serverAddr)
log.Println("Connect your Real TeamSpeak Client to 'localhost:9988' to capture credentials.")
// Map client addresses to server connections (for multiple clients support, simplifying for 1)
// Simple forwarder: We only expect one client for this task.
var clientAddr *net.UDPAddr
buf := make([]byte, 2048)
for {
n, addr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Printf("Read error: %v", err)
continue
}
// Check if packet is from client or server
if clientAddr == nil || addr.String() != clientAddr.String() {
// New client or client packet
// If it's from our known server (unlikely as we don't bind to serverAddr), it's a client.
// But wait, we are listening on localUDP.
// We need to dial the server to forward.
// We'll use a separate socket to talk to the server to keep track of sessions?
// Simplest UDP proxy:
// 1. Receive from Client -> Send to Server (using a dialer)
// 2. Receive from Server -> Send to Client
// Let's reset for new clients to keep it simple
clientAddr = addr
handleClientPacket(conn, serverUDP, buf[:n], addr)
} else {
// Known client
handleClientPacket(conn, serverUDP, buf[:n], addr)
}
}
}
// We need a persistent connection to the server to receive responses
// In a simple loop, we can't easily run two blocking reads (from client and from server) without goroutines.
// Let's structure:
// Main loop reads from Client (ListenUDP).
// On first packet, start a Goroutine that Dials the Server.
// That goroutine reads from Server and writes to Client.
// Main loop writes to Server using that connection.
var serverConn *net.UDPConn
func handleClientPacket(clientConn *net.UDPConn, serverAddr *net.UDPAddr, data []byte, clientAddr *net.UDPAddr) {
// Inspect Packet for clientinit strings (Naive text search)
// clientinit is encrypted in standard flow?
// WAIT. If we proxy, the client and server negotiate encryption.
// We CANNOT decrypt the traffic unless we perform a MitM attack on the Diffie-Hellman handshake.
// TS3 uses ECDH. We cannot derive the shared secret just by passively observing.
// We would need to:
// 1. Intercept `initivexpand2` (Step 2/3/4?)
// 2. Replace the Server's Public Key with OUR Public Key.
// 3. Negotiate SharedSecret1 with Client.
// 4. Negotiate SharedSecret2 with Server (using our Private Key).
// 5. Decrypt Client packet -> Re-encrypt for Server.
// This is significantly complex.
// BUT, `clientinit` might be sent *plaintext* in some legacy modes?
// No, the user's issue is specifically about the encrypted handshake.
// However, `client_version` is sometimes sent in cleartext in the INIT1 packet?
// No, INIT1 is just 4 timestamps + random bytes.
// WAIT. The very first packet is valid?
// `clientinit` is sent AFTER the handshake (Step 6). It is definitely encrypted.
// If the user connects with a real client, the client calculates the SharedSecret using the REAL server's key.
// We verify the packets pass through.
// WE CANNOT READ THE PAYLOAD without a full MitM (replacing keys).
// IS THERE ANOTHER WAY?
// Does the client send version in clear text anywhere?
// Maybe in the UserAgent? (UDP doesn't have headers like HTTP)
// Proposal: Implement Full MitM.
// We already have the ECDH logic implemented in Go!
// We can act as the "Server" to the Client, and "Client" to the Server.
// 1. Client connects to Proxy. Proxy acts as Server.
// 2. Proxy sends its OWN Identity/Key to Client.
// 3. Proxy connects to Real Server.
// 4. Proxy completes handshake with Real Server.
// 5. Client completes handshake with Proxy.
// 6. Proxy decrypts Client's `clientinit`, LOGS IT, then re-encrypts for Server.
// This is the chosen path. It reuses our `handshake.go` and `license.go` logic.
inspectPacket(data)
// Forwarding logic placeholder (Passive won't work for decryption)
}
func inspectPacket(data []byte) {
// Check for "clientinit" ASCII pattern
s := string(data)
if strings.Contains(s, "clientinit") {
log.Printf("[!] FOUND POSSIBLE CLIENTINIT (Plaintext?): %s", s)
}
}