mirror of
https://github.com/coturn/coturn.git
synced 2026-10-24 00:08:57 +03:00
master
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
eda6358d2f |
multiplex-peer: bound the shared per-thread peer demux table (#2017)
## What Moves the `--multiplex-peer` demux table out of `ioa_engine_t` into its own module, `src/apps/relay/mp_peer_table.c/.h`, and gives it two properties it did not have: - **A per-allocation endpoint limit.** New `--multiplex-peer-max-peers` (default 256). Beyond it, CREATE_PERMISSION and CHANNEL_BIND are answered with 508 and SEND indications are dropped. The first rejection per allocation is logged once. - **A reverse index** (session → its endpoints), so `mp_deregister_permission_peers` and `mp_deregister_session_peers` cost that session's own endpoints instead of a full scan of the table shared by every session on the relay thread. ## Why The table is keyed by exact peer IP:port, but permissions are per peer IP, so a single permission covers all 65535 ports of that IP: every SEND indication naming a fresh port added an entry, with no cap and no backpressure. The table is per relay thread and shared, so one busy allocation grows a structure its co-tenants pay for — and every permission expiry and session teardown walked the whole thing. Ephemeral-port churn produces the same growth without anyone trying, so this is also a robustness issue for high-volume deployments. ## Behaviour changes - `--multiplex-peer-max-peers <n>` (1–65535, default 256). Config-file key works too. No effect unless `--multiplex-peer` is set. - An allocation exceeding the limit gets **508** on CREATE_PERMISSION / CHANNEL_BIND. 508 is what this mode already returns for its other capacity limitation (EVEN-PORT), so the code is not new to multiplex-peer clients. - Nothing changes below the limit; the default is far above normal use (a typical allocation registers two endpoints — its peer's RTP and RTCP ports). ## Performance Microbenchmark of the old vs new code paths (arm64, `-O2`, keys precomputed so only table work is timed): | table entries | demux lookup (per datagram) | re-register (per SEND) | permission expiry, 2-endpoint session | |---|---|---|---| | 400 | 99 → 96 ns | 190 → 103 ns | 2.5 µs → 0.068 µs | | 4,000 | 116 → 119 ns | 236 → 125 ns | 9.3 µs → 0.055 µs | | 50,000 | 129 → 133 ns | 270 → 147 ns | 98.7 µs → 0.24 µs | | 500,000 | 501 → 510 ns | 1002 → 510 ns | 986 µs → 1.2 µs | - Data plane is untouched — same map, same lookup; deltas are noise. - The SEND path is ~2× cheaper: the old code did `ur_addr_map_get` **and** `ur_addr_map_put`, and `put` re-scans the bucket. Registering an endpoint the session already owns now returns after the `get`. - Memory: +128 B per endpoint for the index copy, ~310 B per session with endpoints (index array starts at 2 slots because `ioa_addr` is 128 B and the common case is RTP + RTCP). Worst case per allocation is now ~66 KB at the default limit rather than unbounded. The per-thread bound is sessions × limit, so a hard ceiling still wants `--total-quota`. ## Tests - `tests/test_mp_peer_table.c` (new, 8 cases): idempotent re-registration, cross-session conflict, a 4096-port registration run capped at 64, the limit being per-session rather than global, slot reuse after release, per-IP and per-family deregistration, and a deterministic assertion — via a `deregister_ops` counter — that deregistration never touches 200 entries belonging to another session. - `examples/run_tests_multiplex_peer.sh`: server start/stop refactored into functions; adds a run under `--multiplex-peer-max-peers=4` (normal relaying unaffected) and one under `=1` asserting the client sees 508 and the server logs the limit. The second SKIPs where multiplex-peer is inactive (non-Linux). ## Validation - macOS: 18/18 ctest, all `examples/run_tests*.sh`, both fuzzing smoke targets, `make lint` clean. - Linux (Docker): 17/17 ctest; `run_tests.sh`, `run_tests_conf.sh`, `run_tests_mobile.sh`, `run_tests_multiplex_peer.sh`, `run_tests_rfc5780.sh`, `run_tests_stateless_nonce.sh` all green. - Packaged image: `make docker.image dockerfile=debian` + `make test.docker` — 19 Bats tests, 0 failures. ## Reviewer notes - `deregister_ops` is a counter on the table struct read only by the unit test; it is one increment inside a loop that already runs. - `mp_deregister_peer()` had no callers before this change and still has none; it is kept as the exact-key counterpart of the two bulk helpers. - Docs updated: `docs/multiplex-peer.md` (table layout, files-changed table, CLI examples) and `examples/etc/turnserver.conf`. |
||
|
|
dfec35702c |
Return 508, not 400/440, when a well-formed Allocate cannot be satisfied (#2007)
## Summary RFC 8656 §7.2 assigns **508 (Insufficient Capacity)** to a well-formed request the server cannot satisfy; 400 means the request was malformed and 440 means the address family is unsupported. Two config-dependent Allocate paths used the wrong code: - **Dual allocation under `--external-ip`** returned **440**, which tells the client the server lacks an address family entirely — a client may permanently disable IPv6 against a server that supports it fine. Now 508. - **EVEN-PORT under `--multiplex-peer`** returned **400**, which tells a conformant client its request was malformed, so it will not retry without EVEN-PORT. Now 508, matching the sibling missing-shared-socket path in `create_relay_ioa_sockets()`. The `even_port` arm of the dual-allocation check is an unreachable backstop (the attribute loop already rejects ADDITIONAL-ADDRESS-FAMILY + EVEN-PORT in both attribute orderings); it is now commented as such and aligned with the upstream check's 400 and reason text instead of the old 440. Also updated: `run_tests_multiplex_peer.sh`'s randomized-EVEN-PORT retry now matches on error 508, and the stale EVEN-PORT notes in CLAUDE.md and docs/multiplex-peer.md are corrected (the latter claimed EVEN-PORT was silently ignored; it is rejected). ## Validation - macOS and Linux (Docker): clean builds, all unit tests, all example suites (`run_tests`, `_conf`, `_mobile`, `_multiplex_peer`, `_rfc5780`, `_stateless_nonce`) with zero FAIL lines; fuzzing smoke tests. - Both paths verified end-to-end: `turnutils_uclient -Z` against an `--external-ip` server now receives `error 508`, and an EVEN-PORT Allocate against a `--multiplex-peer` server (Linux) receives `error 508` with the matching server-log reason. |
||
|
|
b17f5c482f |
Add --udp-sendmmsg-log to observe egress sendmmsg/UDP-GSO batching (#1927)
## Summary Adds a Linux-only `--udp-sendmmsg-log` flag (mirroring `--udp-recvmmsg-log`) that logs per-relay-thread **egress** batch statistics every 10 s: flush count, total datagrams, average batch occupancy, UDP-GSO engagement (`gso_flushes`/`gso_datagrams`/`gso_frac`), and a per-flush occupancy histogram. ## Motivation `--multiplex-peer` enables `sendmmsg`/UDP-GSO coalescing on the egress path, but there was no way to see whether it actually coalesces anything. While investigating this I confirmed a non-obvious property worth documenting: - Per-session UDP **client** sockets are children of the listener (`parent_s`), and `udp_send_fd()` returns the shared **listener fd** for all of them. Combined with the `recvmmsg`-driven batch window, relay→client **downlink** sends to *different clients* already coalesce into one `sendmmsg` on the listener fd — the listener fd is effectively a shared client-facing send socket. (In non-multiplex mode each allocation has its own relay socket, so a `recvmmsg` drain spans only one session and the downlink batch is a singleton — cross-client batching genuinely requires `--multiplex-peer`.) - UDP-GSO only engages when destination **and** segment size match across a batch, so at low per-flow packet rates (VoIP-style, tens of pps per flow) it rarely fires. This flag makes both effects measurable instead of assumed. ## Example Captured under a `--multiplex-peer --udp-gso` load: ``` udp-sendmmsg stats: flushes=21 datagrams=27 avg_batch=1.29 gso_flushes=0 \ gso_datagrams=0 gso_frac=0.000 hist_1=17 hist_2=2 hist_3_4=2 hist_5_8=0 hist_9_16=0 hist_17_32=0 ``` - `avg_batch` — mean datagrams per flush (1.0 = no coalescing) - `gso_frac` — fraction of datagrams sent via UDP-GSO (~0 = GSO not earning its keep) - `hist_*` — per-flush occupancy histogram Both rise with aggregate pps and sit near the floor on lightly loaded servers. ## Implementation - Counters on `ioa_engine` (behind `#if defined(__linux__)`), bumped once per flush in `udp_sendmmsg_flush()` and once per `recvmmsg` call — no per-datagram cost. - New `--udp-sendmmsg-log` CLI flag + 10 s periodic logger in the engine timer, mirroring the existing recvmmsg stats path. - Docs: a new "Egress Batching (sendmmsg / UDP-GSO) and Observability" section in `docs/multiplex-peer.md`. ## Testing - macOS: build + `ctest` (6/6) + `run_tests.sh` pass (instrumentation is `#if __linux__`, validating flag plumbing + the non-Linux stub path). - Linux (clean Ubuntu 24.04 Docker): build + `ctest` (4/4) + `run_tests.sh` / `run_tests_conf.sh` / `run_tests_multiplex_peer.sh` pass, and a `--multiplex-peer --udp-gso --udp-sendmmsg-log` load run emitted the stats line above. |
||
|
|
e0c37a3aa0 |
Multiplexpeer (#1916)
## Summary Adds **`--multiplex-peer`**, a non-standard relay mode that replaces the per-allocation peer-side port bind with **one shared IPv4+IPv6 UDP socket pair per relay thread**. Sessions are demultiplexed by exact peer IP:port in a per-thread `mp_table`. This lifts the ~16 k allocation cap that the default 49152-65535 relay port range imposes, and dramatically reduces kernel-level UDP receive-buffer drops under high pps. Design and trade-offs: [docs/multiplex-peer.md](docs/multiplex-peer.md). ## What changes ### Server (turnserver) - **`--multiplex-peer`** (cross-platform) — enable the shared per-thread relay sockets. Replaces the per-session port bind. Implies sendmmsg batching on Linux and default-enables `--udp-recvmmsg` (override with `--udp-recvmmsg=0`). Incompatible with EVEN-PORT — those Allocates are rejected with 400. - **`--multiplex-peer-port <port>`** (cross-platform, default 3480) — base port; thread `i` binds `<base>+2i` (IPv4) and `<base>+2i+1` (IPv6). A 4-thread server consumes 8 ports. - **`--udp-gso`** (Linux-only CLI) — UDP-GSO (`UDP_SEGMENT` cmsg) on the relay send path. Requires `--multiplex-peer` (which is what enables the sendmmsg batching GSO piggybacks on); passing `--udp-gso` alone is a silent no-op. - **CLI surface tightened**: `--udp-recvmmsg`, `--udp-recvmmsg-log`, `--udp-gso` and their fields are now `#if defined(__linux__)` — absent from `--help`, rejected with `unrecognized option`, and the code paths compile out on macOS/Windows. - **Windows portability**: `SO_REUSEPORT` in `mp_open_socket` wrapped in `#ifdef` (MSVC's Winsock doesn't define it; REUSEPORT was defensive anyway because the per-thread port layout is unique by construction). - **`--sock-buf-size` honoured at startup**: the shared multiplex-peer relay socket now calls `set_ioa_socket_buf_size` in `mp_open_socket` so the configured rcvbuf is in effect from the moment the socket exists, not deferred to the first Allocate. ### turnutils_uclient (loadgen) - **`--no-even-port`** — force `ep = -1` on Allocate. The default path randomly attaches EVEN-PORT (with no-R bit) even under `-c`, which `--multiplex-peer` strictly rejects with 400; this flag makes alloc-flood runs against multiplex-peer deterministic. - **Legacy `timer_handler` now wraps the per-tick send batch with `uclient_send_batch_begin/_end`** — without this, runs with `--sender-threads 0` (the default for `-m < 4`) silently fell through every send to plain `send(2)`. strace A/B: 205 k `sendto` → 61 k `sendmsg` (GSO) + 4 k `sendmmsg` + small `sendto` residual for control. ## Measured impact (3-droplet DigitalOcean, c-4 / 4 vCPU, 8 concurrent UDP streams, 45 s) | | baseline | `--udp-recvmmsg` | `--multiplex-peer` | `--multiplex-peer --udp-gso` | |---|---:|---:|---:|---:| | Server NIC rx pps (UDP relay both legs) | 350 k | 334 k | 326 k | 294 k | | Server `UdpInDatagrams` pps | 279 k | 292 k | 300 k | 294 k | | **Server `UdpRcvbufErrors` pps** | **71 k** | 42 k | 26 k | **0.3 k (−99.6 %)** | | **`turnserver` process CPU** | **387 %** | 205 % | 283 % | **133 % (−65 %)** | | Server host idle | 22 % | 49 % | 41 % | **68 %** | Same loadgen-side packet rate (~2 M pps reported by uclient `send_pps` after the legacy-path batching fix). Iteration log: [docs/PerformanceIterationLog.md](docs/PerformanceIterationLog.md). ## Test plan - [x] `ctest --test-dir build` — 3/3 pass (test_ioaddr, test_stun_msg, test_http_server) on macOS + Linux. - [x] `examples/run_tests.sh` — 4 protocols + 4 threaded + load-gen smoke on Linux; 4 protocols on macOS. - [x] `examples/run_tests_conf.sh` — same coverage, conf-driven. - [x] `examples/run_tests_multiplex_peer.sh` — UDP/TCP/TLS/DTLS via `--multiplex-peer --multiplex-peer-port=35000` on macOS + Linux. - [x] Flag matrix smoke on macOS: `--multiplex-peer`, `--multiplex-peer-port=42000`, `--multiplex-peer --udp-gso` (no-op), `uclient --no-even-port`, `uclient --listener-threads N --sender-threads M` — all pass; `--udp-recvmmsg` / `--udp-gso` correctly rejected with `unrecognized option`. - [x] Flag matrix smoke on Linux (Docker): same + `--udp-recvmmsg` accepted, `--multiplex-peer` auto-enables `--udp-recvmmsg`, `--udp-recvmmsg=0` overrides the auto-enable. - [x] Windows compile fix verified — `SO_REUSEPORT` no longer referenced unconditionally. - [x] 3-droplet perf matrix completed; per-hop UDP counters captured. ## Docs updated - New: [docs/multiplex-peer.md](docs/multiplex-peer.md) - [README.turnserver](README.turnserver): full entries for `--multiplex-peer`, `--multiplex-peer-port`, `--udp-gso`; clarified `--udp-recvmmsg` auto-enable semantics. - [README.turnutils](README.turnutils): added `--no-even-port`, plus previously-undocumented `--listener-threads` / `--sender-threads` loadgen pool flags. - [examples/etc/turnserver.conf](examples/etc/turnserver.conf): commented `udp-recvmmsg`, `udp-recvmmsg-log`, `udp-gso`, `multiplex-peer`, `multiplex-peer-port` keys with one-paragraph descriptions and pointer to `docs/multiplex-peer.md`. - Man pages regenerated via `./make-man.sh`. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |