diff --git a/internal/client/client.go b/internal/client/client.go index c642a36..6e8890f 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -37,6 +37,12 @@ type Client struct { PongPacketID uint16 // Type 0x05 AckPacketID uint16 // Type 0x06 + // Ping RTT tracking + PingSentTimes map[uint16]time.Time // Map PingPacketID -> Time sent + PingRTT float64 // Rolling average RTT in ms + PingDeviation float64 // Rolling deviation in ms + PingSampleCount int // Number of samples for rolling avg + // State Connected bool ServerName string @@ -70,6 +76,9 @@ func NewClient(nickname string) *Client { VoiceDecoders: make(map[uint16]*opus.Decoder), CommandQueue: make(map[uint16]*protocol.Packet), ExpectedCommandPID: 0, + PingSentTimes: make(map[uint16]time.Time), + PingRTT: 0, + PingDeviation: 0, done: make(chan struct{}), } } @@ -187,6 +196,9 @@ func (c *Client) sendPing() error { pkt.Data = encData copy(pkt.Header.MAC[:], mac) + // Record send time for RTT calculation + c.PingSentTimes[pkt.Header.PacketID] = time.Now() + log.Printf("Sending proper Encrypted Ping (PID=%d)", pkt.Header.PacketID) return c.Conn.SendPacket(pkt) } diff --git a/internal/client/commands.go b/internal/client/commands.go index ba7c7b3..1051fab 100644 --- a/internal/client/commands.go +++ b/internal/client/commands.go @@ -459,11 +459,20 @@ func (c *Client) processCommand(data []byte, pkt *protocol.Packet) error { 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'...") + log.Println("Server requested connection info. Sending 'setconnectioninfo'...") cmd := protocol.NewCommand("setconnectioninfo") - cmd.AddParam("connection_ping", "50") - cmd.AddParam("connection_ping_deviation", "5") + + // Use real ping values if available, otherwise default to 50ms + pingMs := c.PingRTT + pingDev := c.PingDeviation + if pingMs == 0 { + pingMs = 50.0 // Default before first measurement + pingDev = 5.0 + } + + cmd.AddParam("connection_ping", fmt.Sprintf("%.4f", pingMs)) + cmd.AddParam("connection_ping_deviation", fmt.Sprintf("%.4f", pingDev)) // Detailed stats for each kind as seen in ts3j (KEEPALIVE, SPEECH, CONTROL) kinds := []string{"keepalive", "speech", "control"} diff --git a/internal/client/packet.go b/internal/client/packet.go index 622feb4..3db9113 100644 --- a/internal/client/packet.go +++ b/internal/client/packet.go @@ -4,6 +4,8 @@ import ( "bytes" "encoding/binary" "log" + "math" + "time" "go-ts/pkg/protocol" ) @@ -114,8 +116,59 @@ func (c *Client) handlePacket(pkt *protocol.Packet) error { c.Conn.SendPacket(pong) case protocol.PacketTypePong: - // Server acknowledged our Ping - log.Printf("Received Pong for sequence %d", pkt.Header.PacketID) + // Server acknowledged our Ping - calculate RTT + receivedAt := time.Now() + + // Decrypt Pong body if needed to get the PingID it's acknowledging + var pongData []byte + if pkt.Header.FlagUnencrypted() { + pongData = pkt.Data + } else 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(&pkt.Header, false) // Server->Client + meta := make([]byte, 3) + binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID) + meta[2] = pkt.Header.Type + decrypted, err := protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:]) + if err == nil { + pongData = decrypted + } + } + + // Extract PingID from Pong body + var pingID uint16 + if len(pongData) >= 2 { + pingID = binary.BigEndian.Uint16(pongData[0:2]) + } else { + pingID = pkt.Header.PacketID // fallback + } + + // Calculate RTT if we have the send time + if sentTime, ok := c.PingSentTimes[pingID]; ok { + rtt := receivedAt.Sub(sentTime).Seconds() * 1000 // RTT in ms + delete(c.PingSentTimes, pingID) + + // Update rolling average using Welford's algorithm + c.PingSampleCount++ + if c.PingSampleCount == 1 { + c.PingRTT = rtt + c.PingDeviation = 0 + } else { + oldMean := c.PingRTT + c.PingRTT = oldMean + (rtt-oldMean)/float64(c.PingSampleCount) + // Rolling deviation: exponential smoothing + c.PingDeviation = c.PingDeviation*0.9 + math.Abs(rtt-c.PingRTT)*0.1 + } + + log.Printf("Received Pong for Ping %d: RTT=%.2fms, AvgRTT=%.2fms, Dev=%.2fms", + pingID, rtt, c.PingRTT, c.PingDeviation) + } else { + log.Printf("Received Pong for unknown Ping %d", pingID) + } case protocol.PacketTypeAck: // Server acknowledged our packet var data []byte