Codebase audit — ts3-client
First audit: 2026-09-11. Revisited and worked through: 2026-09-25. Every finding below was
either fixed (commit given) or closed with a reason. Logging (§3) was left out on purpose.
State after the fixes: mvn -o test passes (177 tests: 25 in ts3j, 135 core, 11 desktop,
6 swing), on Java 25 and 26. The Android release build (R8 over core and ts3j) passes. The
Swing client was run against the test server on Java 25: it connected, loaded the tree and
chat logs, and wrote its profile owner-only.
1. Fixed
Tests and build
| Finding |
Commit |
The importer test failed on every run after the first. The cause was not the real profile: the POM already points user.home at target/test-home, but that folder outlives a run, so the first run's identity was still there |
ba5de5a |
The ts3j DNS test resolved voice.teamspeak.com for real and failed when DNS did not answer |
ts3j Test SRV lookup…, acb3efa |
| README architecture and features out of date |
51f19c2 |
| Java 26 required for no reason; it now needs 25 (LTS), and builds, tests and runs on it |
85c3ae6 |
| Missing tests: channel tree, event handler, settings round-trip and migration |
71ac2c8 |
Connection lifecycle and threading
| Finding |
Commit |
An action racing a disconnect showed NullPointerException as the error; 20 raw threads. Actions now capture the socket and run on a pool that lives as long as the connection |
f350b19 |
connect() on a live instance leaked socket and audio; fields shared across threads were not volatile |
d28bb57 |
| Permission errors were recognised by substring; now by the server's error id |
d074ffa |
HotkeyEngine locked on another class's list; Hotkeys.all() now returns a snapshot |
5b1809c |
| Two quick tab switches could interleave the microphone hand-over |
665316d |
MainFrame was also the app controller: microphone ownership, push-to-talk, away and nickname fan-out moved to core/session/Sessions |
bf07f0b |
Model and events
| Finding |
Commit |
buildTree() refilled lists shared with the UI; node fields not volatile. Lists are now published whole and unmodifiable. Whether a channel is full is counted from the model |
67f8f46 |
selfPermissionValue scanned every permission; primaryServerGroupName unused |
8c735a0 |
| A cleared avatar, description or away message was ignored |
7cb4f47 |
Voice
| Finding |
Commit |
Fixed 60 ms jitter delay instead of TS3's adaptive buffer. The stashed libspeex port is now in VoiceStream, driven as TS3 drives it. A simulated 0–60 ms jitter stops concealing after about 3 s |
0083dc8 |
| A playback line kept open for every client that ever spoke |
6234ec6 |
| A dead playback line silently muted its speaker for the whole session |
12ca585 |
CaptureVoiceInput.stop() tore down while the capture loop was still running |
8e9c461 |
Chat logs
| Finding |
Commit |
| Multi-line messages lost their continuation lines in history |
f9b2f4d |
| History read whole, ever-growing logs; now the last 4 MiB |
a5c4ba1 |
appendPrivateMessage duplicated appendMessage |
1a3a3e3 |
| Logs could not be turned off or moved (new Options → Chat page) |
d7261c9 |
Chat logs only worked with admin rights: the server id came from serverinfo, which guests may not use. It turned out to be base64(SHA-1(the server's handshake key)), as the official client derives it. Found while checking the "serverUniqueId fallbacks can go" finding |
ts3j Derive the server's unique id…, eeccb49 |
Persistence, file transfers, UI
| Finding |
Commit |
| Profile files (private keys, server passwords) readable by everyone, and truncated by a crash mid-write. Now written privately and atomically, and the profile folder is owner-only |
3cdade9 |
Three copies of the enum parser in Settings |
48a0032 |
| A stalled file server hung a download forever |
ae8d1f7 |
| Collapsed channels reopened on every tree rebuild |
f30cecf |
2. Closed without a change, or still open
- Chat-log folders "may contain
/": they cannot. The value encoded is itself Base64 text,
and no 6-bit group of such bytes reaches 62 or 63, the codes for + and /.
rootMessage() hides the command: every caller names the action in front of it.
- Not tested:
TeamspeakConnection's lifecycle against a fake socket (the smoke test
above covers connect only); the idle release and dead-line replacement in
PerSpeakerPlayout; the jitter buffer with real voice from another client.
git stash@{0} ("jitter buffer test") is now fully ported, except FEC decoding (TS3
does not use FEC either). It can be dropped.
3. Left out on purpose
- Logging. There are still 60 swallowed exceptions and no log output.