fix(server): bind UDP media to the TCP port so self-host needs one forward rule
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled

media_port defaulted to 0 (OS-assigned) and --port only set the TCP bind_port,
so the UDP relay bound a random high port and advertised it to clients in HELLO.
Self-hosters forwarding only 8384/udp saw connect-OK-but-no-voice, contradicting
docs/deployment.md (control and media share one port). Media now follows
bind_port when media_port is unset; 0=OS-assigned survives when bind_port is also
0 so ephemeral-port tests are unaffected. Banner now reads TCP :8384 UDP :8384.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 12:48:13 +02:00
parent 5e18dfa1c9
commit 483f889910
3 changed files with 23 additions and 3 deletions

View File

@@ -10,6 +10,22 @@ up instantly. Newest status at the top.
## ▶ Where we left off / next action ## ▶ Where we left off / next action
- **Done (2026-06-22):** **UDP media now shares the TCP port (self-host port-forward fix).** Symptom: a
remote self-hosted server (`iamtalon.me:8384`, TCP+UDP 8384 forwarded) accepted TCP connections but
passed no voice. Root cause: `Config::media_port` defaulted to `0` = OS-assigned, and `main.cpp`'s
`--port` only set `bind_port` (TCP) — so the UDP relay bound a *random high port*, advertised it to
clients in HELLO (`udp_port`), and clients sent voice there. With only `8384/udp` forwarded those
packets were dropped → connect OK, no audio. This contradicted `docs/deployment.md` ("Control and media
share one port number on TCP+UDP"). **Fix (`server/src/server.cpp`):** media follows bind_port when
`media_port == 0``media_want = cfg_.media_port != 0 ? cfg_.media_port : cfg_.bind_port`. The
`0 = OS-assigned` escape hatch survives when `bind_port` is also 0, so tests that bind ephemeral ports
are unaffected (kept the logic in server.cpp rather than hardcoding 8384 as the default, which would
collide parallel tests on UDP 8384). Banner now reads `TCP :8384 UDP :8384`. Build + `ctest --preset
dev` green (24/24); live-verified banner with `--port 8390``UDP :8390`. **Action for self-hosters:**
redeploy and confirm the startup banner shows matching TCP/UDP ports; the existing single forward rule
is now correct. If voice still fails, watch the server's rate-limited `[media] dropped frames —
unmapped-endpoint=…` line (NAT source-port rewrite would be the next suspect).
- **Awaiting on-device verification (2026-06-22):** **iOS real echo cancellation / noise suppression - **Awaiting on-device verification (2026-06-22):** **iOS real echo cancellation / noise suppression
via native VPIO.** Root cause of "voice chat doesn't sound like a call" (echo + no NR): real iOS via native VPIO.** Root cause of "voice chat doesn't sound like a call" (echo + no NR): real iOS
AEC/NS/AGC come only from Apple's Voice-Processing I/O unit (VPIO), but the core uses miniaudio's AEC/NS/AGC come only from Apple's Voice-Processing I/O unit (VPIO), but the core uses miniaudio's

View File

@@ -69,8 +69,12 @@ int Server::run() {
// ── UDP media relay (M2) ───────────────────────────────────────────────── // ── UDP media relay (M2) ─────────────────────────────────────────────────
auto media_relay = std::make_shared<MediaRelay>(io, registry); auto media_relay = std::make_shared<MediaRelay>(io, registry);
if (!media_relay->bind(cfg_.media_port)) { // Control and media share one port number on TCP+UDP (docs/deployment.md): when media_port
std::fprintf(stderr, "[server] failed to bind UDP media port %u\n", cfg_.media_port); // is left at 0, follow bind_port so a single forward rule covers both. If bind_port is also 0
// (tests), this stays 0 and the OS picks the UDP port (reported via on_media_ready).
uint16_t media_want = cfg_.media_port != 0 ? cfg_.media_port : cfg_.bind_port;
if (!media_relay->bind(media_want)) {
std::fprintf(stderr, "[server] failed to bind UDP media port %u\n", media_want);
return 1; return 1;
} }
media_relay->start(); media_relay->start();

View File

@@ -18,7 +18,7 @@ struct Config {
std::string server_name = "VoiceCat Server"; std::string server_name = "VoiceCat Server";
std::string data_dir = "voicecat-data"; std::string data_dir = "voicecat-data";
uint16_t bind_port = 8384; // 0 = let OS pick (useful for tests) uint16_t bind_port = 8384; // 0 = let OS pick (useful for tests)
uint16_t media_port = 0; // M2 UDP media port; 0 = OS-assigned uint16_t media_port = 0; // M2 UDP media; 0 = follow bind_port (or OS-pick if that's 0)
bool allow_guests = true; bool allow_guests = true;
// Called with the actual bound TCP port once the acceptor is ready. // Called with the actual bound TCP port once the acceptor is ready.
std::function<void(uint16_t)> on_ready; std::function<void(uint16_t)> on_ready;