1
0
mirror of https://github.com/coturn/coturn.git synced 2026-10-24 00:08:57 +03:00

4 Commits

Author SHA1 Message Date
Pavel Punsky
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`.
2026-07-26 19:56:41 -07:00
Pavel Punsky
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.
2026-07-25 22:51:24 -07:00
Pavel Punsky
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.
2026-05-30 17:30:45 -07:00
Pavel Punsky
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>
2026-05-17 19:16:55 -07:00