Commit Graph
21 Commits
Author SHA1 Message Date
erikbrockoandClaude Opus 5.5 004aa41732 Pad a short RSA puzzle answer instead of failing the connect
An answer with a leading zero byte comes out of toByteArray() as 63 bytes,
and copying it started one byte too far, failing about one connect in 256.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:17:49 +00:00
erikbrockoandClaude Opus 5.5 0549b6bf02 Sign myTeamSpeak identity into clientinit
Adds an authenticator hook that appends the myTeamSpeak parameters to
clientinit/updatemytsid, and the per-connection challenge they sign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:04:55 +00:00
erikbrockoandClaude Opus 5.5 1430782e2a Send the microphone's voice as whispers to a target
setWhisperTarget routes voice into VOICE_WHISPER packets to channels and
clients until cleared; a voice session under way ends on its old route
first. Whisper packets carry their own packet id and get the redundant
copies voice does.

WhisperTargetMultiple sized its two counts as two bytes each, leaving
zero bytes after the voice data, and read the client ids by the channel
count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:18:07 +00:00
erikbrockoandClaude Opus 5.5 2d6f5cdf78 Send every ready live-capture frame per tick and cap the timer's catch-up burst
A mic running faster than the 20 ms timer filled the queue until frames were
dropped; live sources now drain each tick. The catch-up clamp was inverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:14:42 +00:00
erikbrockoandClaude Opus 5.5 a4ce0feb61 Report a minute's bandwidth as bytes per second
setconnectioninfo sent the bytes of the whole last minute where the
server expects the minute's average rate, so others saw our bandwidth
60 times too high. PacketStatistics gains the per-second averages, and
the packets-last-minute count looked at the last second only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:26:59 +00:00
erikbrockoandClaude Opus 5.5 90bf6e030c Resend voice packets for loss resilience
A microphone can ask for every voice packet to be sent again, a number
of times at a fixed spacing. The copies are the same encrypted packet,
so listeners keep whichever arrives first. A TS3 server forwards them,
and a jitter buffer that sees them arrive late grows to wait for them:
the sender buys its listeners a deeper buffer, trading latency for
riding out loss, with native clients as much as with others.

Copies go out from the 20 ms microphone timer, so the spacing is best a
whole number of ticks. Copies of a connection that is gone are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:17:13 +00:00
erikbrockoandClaude Opus 5.5 4cc27db0e8 Fail a refused connect with the server's reason
An error answering the handshake, such as "too many clones already
connected", used to surface only as "disconnected while waiting for
CONNECTED state". connect() now fails with the server's message, and
the CommandException carrying its error id is the cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:12:50 +00:00
erikbrockoandClaude Opus 5.5 d89a02957a Test SRV lookup against stubbed records, not the live DNS
The test resolved voice.teamspeak.com for real, so it failed whenever
that name, or the network, did not answer. It now checks the ordering
and parsing with a stubbed resolver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:03:41 +00:00
erikbrockoandClaude Opus 5.5 c47bbdf96c Derive the server's unique id from its handshake key
TS3 derives a virtual server's unique identifier from its public key the
same way as a client's: base64(SHA-1(key)). The server never tells a
guest otherwise, as serverinfo is refused and neither initserver nor
notifyserverupdated carries it. The socket now keeps the key from
initivexpand(2) and offers getServerUid().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:35:41 +00:00
erikbrockoandClaude Opus 5.5 8273a27160 Ignore Maven's target/ directory
The pattern read targetId/, so build output showed up as untracked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:26:21 +00:00
erikbrockoandClaude Opus 5.5 4aa9341ec4 Drop the connection when a command never gets through
The server handles commands in order and holds back everything sent
after a lost one, so giving up on a command now ends the connection, as
in the official client. A connection lost without either side saying
goodbye (a timeout, or such a command) is now reported to listeners as
a disconnect ("connection lost").

- A command's acknowledgement is awaited in the background: ensureSent
  is often called from the packet handler thread, which must keep
  handling while resends go on. A command that fails makes way for the
  next one.
- One packet that fails to handle no longer ends the handler thread,
  which left everything after it unhandled while ACKs and pings kept the
  connection looking alive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:08:09 +00:00
erikbrockoandClaude Opus 5.5 5a5fb6baaf Encrypt each packet with its own EAX cipher
TS3 derives a packet's key and nonce from its id, so resending a packet
encrypts it under the same pair again. BouncyCastle has since learned to
refuse that on a cipher that has already used the pair ("cannot reuse
nonce for EAX encryption"), which broke resends on the current version
the Android build uses. A fresh cipher per packet keeps them working.

Moves to bcprov-jdk18on 1.86, the maintained line, so every platform runs
the same BouncyCastle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:42:48 +00:00
erikbrockoandClaude Opus 5.5 3625cc24cf Look up SRV records without requiring JNDI
TS3DNS resolved SRV records through JNDI's DNS provider, which Android
does not have. The JNDI code moves into its own class that is only
loaded where JNDI exists; elsewhere a server is reached by its address,
as when it publishes no SRV record. TS3DNS.setSrvLookup lets a platform
supply its own resolver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:13:09 +00:00
erikbrockoandClaude Opus 5.5 2018c1c593 Resend like the official client; keep the handshake consistent under loss
- Resend unacknowledged packets on the official client's schedule (mean RTT
  + 3 sigma + 50 ms over the last 20 samples, x1.4 per resend) and give up
  after 18 s, instead of a fixed 1 s x 30.
- Resend INIT1 packets until the server answers; they are never ACKed.
- Answer each server Init1 step once: a duplicated step-3 reply sent a second
  clientinitiv with a new alpha, leaving client and server with different
  keys (MAC check failures).
- Disconnect without blocking on an ACK the server may never send.
- Fail waitForState promptly when the connection drops, and name the packet
  in decrypt errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 04:58:21 +00:00
erikbrockoandClaude Opus 5.5 45cb8fd559 Send clientinit without waiting for clientek's ACK
The server does not acknowledge a resent clientek, so losing its first ACK
stalled the handshake: we kept resending clientek and never got to
clientinit. Send clientek without blocking (resends continue in the
background via the new writePacketAsync) and follow with clientinit right
away; the server cannot decrypt clientinit before it has processed
clientek, so the two stay in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 23:21:44 +00:00
erikbrockoandClaude Opus 5.5 765bfb25fd Fall back to the handshake key when a packet fails to decrypt
The server encrypts initivexpand2 (COMMAND 0 and 1) with the handshake
key. If our ACK for one of those fragments is lost, its retransmit arrives
after we switched to the session key, fails with "mac check in EAX
failed", is never acknowledged and blocks the server's command stream
until it drops us. Common on lossy links such as mobile data.

Retry generation 0 packets that fail under the session key with the
handshake key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 23:21:44 +00:00
erikbrockoandClaude Opus 5.5 b335c2c511 Tolerate an empty client_servergroups in Client.getServerGroups
Some servers list a client with an empty client_servergroups; parsing it
threw NumberFormatException and aborted iterating the whole client list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:30:48 +00:00
erikbrockoandClaude Opus 5.5 6fe787de30 Resolve incoming packet generations relative to the newest packet
RemoteCounterFull only knew a 100-packet window. A packet that arrived more
than 100 IDs late - typically a retransmit after a loss in the large burst a
busy server sends on connect - was assumed to belong to the next generation,
as was packet ID 0 right after a wrap. The generation feeds the EAX key, so
those packets failed with "mac check in EAX failed", were never acknowledged
and stalled the command stream.

Place each ID in the generation closest to the newest packet received, and
remember the last 32768 positions for duplicate detection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:09:09 +00:00
erikbrocko 49f2ce874d Request voice properties in clientlist
Without -voice the server omits client_input_muted/output_muted and
the hardware flags entirely, so a bare clientlist leaves callers
unable to tell "false" from "not reported" and defaults them wrong.
2026-08-17 08:42:59 +00:00
erikbrockoandClaude Opus 5 0e5724b877 Keep the virtual server properties from initserver
initserver announces the whole virtual server (its name above all), but only
the client and server ids were taken from it and the rest was dropped. The
name is not obtainable elsewhere: servergetvariables answers without it, and
notifyserveredited only fires when someone edits the server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:15:26 +00:00
erikbrockoandClaude Opus 5 dce4e68150 Drop the commons-lang and dnsjava dependencies
Both were pulled in for a single call each: commons-lang for
ConstructorUtils.invokeConstructor when building a packet handler, and
dnsjava for the _ts3._udp SRV lookup. Together they added over a
megabyte to a shaded jar (and dnsjava drops jnamed/update classes into
the jar root). Plain reflection and JNDI's dns: provider cover both.

Also drop the BouncyCastleProvider registration: every digest here is
requested by a name the SUN provider already answers, so the only thing
it accomplished was keeping the whole JCE provider, the post-quantum
suite and the X.509 stack reachable for a shaded-jar tree-shaker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 23:46:56 +00:00