mirror of
https://github.com/coturn/coturn.git
synced 2026-10-24 00:08:57 +03:00
Closes #1999. ## Problem A structurally valid STUN request from a spoofed UDP source commits real resources before any authentication: a child socket in the listener's address map plus an ~18KB `ts_ur_super_session` — allocated solely to store the random 401-challenge nonce — kept alive for 60 seconds by the to-be-allocated timer. Early packet validation (#1768) doesn't help against well-formed packets, and `--unauthorized-ratelimit` suppresses only the 401 *responses*, after the state already exists. At 10k spoofed pps that is hundreds of thousands of live sessions (multiple GB) from traffic that never authenticates. ## Approach With `--stateless-nonce` (off by default) the challenge nonce becomes an **authenticated timestamp cookie** instead of stored random state: ``` nonce = <8 hex chars: 32-bit issue time> || <16 hex chars: HMAC-SHA256(K, "<client-addr>|<ts-hex>") truncated> ``` Because the server can *validate* a nonce (parse timestamp → recompute MAC with a constant-time compare → check age against `--stale-nonce`, default 600s, with a 5s future-skew allowance), three things follow: 1. **Listener fast path** (`udp_stateless_nonce_fast_path`, `dtls_listener.c`): MESSAGE-INTEGRITY-less requests from unknown UDP sources are answered with the 401 challenge directly from the listener — no socket, no session. Packets the relay would silently ignore for a fresh source (indications, unbound channel data, STUN failing the full check) are dropped with no state either. 2. **Fresh-session nonce acceptance** (`check_stun_auth`): the client's authenticated retry lands on a brand-new session, which accepts a presented nonce whose MAC verifies for that client address and whose issue time is within the lifetime. Stale-nonce expiry is anchored to the real issue time. 3. **Challenge-session teardown**: a UDP session whose only output was a 401/438 (or whose challenge the ratelimit suppressed) is torn down immediately instead of idling for 60s. This bounds memory against floods that attach a garbage MESSAGE-INTEGRITY and therefore must travel the session path. Result: zero attacker memory cost for MI-less floods; one transient, immediately-freed session per packet for garbage-MI floods. `--stateless-nonce-secret=<secret>` (implies the flag) additionally derives the key via `K = SHA-256("coturn-stateless-nonce-v1" || secret)` so a fleet sharing the secret cross-validates nonces across restarts and load balancers. Without it the key is ephemeral per process. ## Wire transparency - The fast-path 401 replicates the session path byte-for-byte: same attributes in the same order (NONCE, REALM, THIRD-PARTY-AUTHORIZATION for oauth, SOFTWARE, FINGERPRINT), including realm selection by ORIGIN on ALLOCATE, plus the same `--unauthorized-ratelimit` suppression and Prometheus 401 counters. - Anything the fast path cannot answer identically falls through to the untouched session path: BINDING (RFC 5780), ALLOCATE under alternate/aux-server redirection, REFRESH under `--mobility`, CONNECTION-BIND, and all MI-carrying requests. - Nonce reuse within its lifetime is how RFC 8489 long-term auth already works (per-request freshness comes from MESSAGE-INTEGRITY over the random transaction ID); binding the nonce to the client address is strictly stronger than a stored random value. The nonce grows from 16 to 24 chars when the flag is on — RFC 8489 requires clients to echo nonces opaquely up to 128 chars. - With the flag off, every changed code path short-circuits to stock behavior. Design notes, threat model, and limitations: `docs/stateless-nonce.md`. ## Reviewer notes - The branch is four commits telling the story in order: the feature with windowed derivation, an int→bool type cleanup around the touched code, the switch to the timestamp-cookie format (exact expiry, no window scanning), and the shared fleet key. Happy to squash if preferred. - `NONCE_MAX_SIZE` grows to hold both nonce formats, and nonce emitters now use `strlen()` instead of the fixed `NONCE_MAX_SIZE - 1` (which would otherwise pad the shorter legacy random nonce). - `stun_is_command_message_full_check_str()` gained `bool` parameters (was `int`), rippling through its nine in-repo callers. - One pre-existing test-harness fix rides along: `run_tests.sh`/`run_tests_mobile.sh` raced clients against a still-initializing server on macOS (fixed `sleep 2` instead of log polling); both now poll on both platforms. The CLAUDE.md container command also gained `libmicrohttpd-dev` and a `git safe.directory` line it needs on a clean image. - Known limitation: key rotation for `--stateless-nonce-secret` means restarting with a new secret (one standard 438 re-auth per client); sign-with-new/verify-with-both is documented as future work. ## Validation - New Unity suite `tests/test_stateless_nonce.c`: format, determinism, per-input divergence, single-character tampering across the whole cookie, malformed inputs, age/skew boundaries, KDF determinism and secret binding, and a cross-server test (two keys from one secret validate each other's nonces; a third secret does not). - New end-to-end suite `examples/run_tests_stateless_nonce.sh`: standard relay workload with the feature on over UDP/TCP (+TLS/DTLS and threaded variants on Linux), asserting the listener fast path actually engaged and the secret-derived-key startup markers. - Full battery green on macOS and in a clean Linux container: cmake build + 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`, fuzzing smoke (ASan targets 0/1), and the packaged Docker image bats tests (19 tests, 0 failures).