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

17 Commits

Author SHA1 Message Date
Pavel Punsky
142dd93b30 Add --stateless-nonce: bound memory under valid-structure STUN floods (#1999) (#2002)
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).
2026-07-25 16:16:01 -07:00
Pavel Punsky
b1d5c467f3 fuzzing: use hex escapes for HTTP EOH dictionary entry (#1905)
## Summary

`fuzzing/stun.dict` line 147 used C-style `\r\n\r\n` for the HTTP
end-of-headers keyword:

```
kw_http_eoh="\r\n\r\n"
```

libFuzzer's `ParseDictionaryFile` only accepts three escape sequences
inside quoted entries: `\\`, `\"`, and `\xAB` hex. `\r` / `\n` are
unrecognized, so a local fuzz run aborts dictionary load with:

```
ParseDictionaryFile: error in line 147
                kw_http_eoh="\r\n\r\n"
```

Replace with the hex form used by the other 111 entries in the file:

```
kw_http_eoh="\x0d\x0a\x0d\x0a"
```
2026-05-08 20:26:46 -07:00
Pavel Punsky
cb701a47b4 Add fuzz coverage for integrity helpers (#1888) 2026-04-30 22:32:50 -07:00
Pavel Punsky
247118d1b4 Add deterministic challenge-response builder to FuzzStun (#1886)
stun_is_challenge_response_str in src/client/ns_turn_msg.c only descends
into its three inner stun_attr_get_first_by_type_str calls when the
input is an error response with err_code 401 or 438 *and* a REALM
attribute *and* a NONCE attribute. The OAuth branch additionally
requires STUN_ATTRIBUTE_THIRD_PARTY_AUTHORIZATION.

The fuzzer-driven path in harness_attr_iter calls the predicate every
iteration but the conjunction of conditions is too specific for
libFuzzer to discover from binary mutation alone — OSS-Fuzz introspector
flags 9 unreached callsites on
stun_attr_get_first_by_type_str gated on this function.

Add harness_challenge_response_builder that constructs six deterministic
message variants on every iteration and runs each through the predicate:

  - 401 with REALM + NONCE                      (canonical success)
  - 401 with REALM + NONCE + THIRD-PARTY-AUTH   (OAuth branch)
  - 438 with REALM + NONCE                      (438 disjunct)
  - 401 with REALM only                         (NONCE-missing path)
  - 401 with no REALM                           (REALM-missing path)
  - 400 with REALM + NONCE                      (wrong err_code path)

Each variant runs once with a non-NULL oauth pointer and once with NULL
to cover both branches of the optional output. Realm / nonce /
server-name lengths and the transaction id are derived from fuzz bytes
so iterations stay meaningfully distinct.

Verified by stand-alone harness:
  - 401+REALM+NONCE returns true with attrs copied out, oauth=false
- 401+REALM+NONCE+TPA returns true with oauth=true and server_name
populated — confirming all three inner get_first_by_type_str callsites
and the OAuth disjunct are now exercised.
2026-04-28 14:30:25 -07:00
Pavel Punsky
8952415609 Seed address-mapping table in fuzz initializer (#1885)
map_addr_from_public_to_private and map_addr_from_private_to_public in
src/client/ns_turn_ioaddr.c walk a static public_addrs[] table of size
mcount. Without an explicit ioa_addr_add_mapping call mcount stays 0 for
the entire fuzz process, so the loop body — including the
addr_eq_no_port call it gates — is dead code in every fuzz iteration.
OSS-Fuzz introspector flags this as 19 unreached callsites under
map_addr_from_public_to_private and 4+4 under addr_eq_no_port.

Extend the existing shared LLVMFuzzerInitialize in FuzzOpenSSLInit.c
(linked into both FuzzStun and FuzzStunClient via FUZZ_COMMON_SOURCES)
to register two synthetic public<->private mapping pairs — one v4
(192.0.2.1 <-> 10.0.0.1) and one v6 (2001:db8::1 <-> fd00::1) — once at
startup. The header comment for ioa_addr_add_mapping requires
single-threaded init before fuzzing begins, which matches exactly when
LLVMFuzzerInitialize runs.

Verified by stand-alone harness: after init,
stun_attr_get_first_addr_str on a XOR-MAPPED-ADDRESS attribute holding
192.0.2.1:443 returns 10.0.0.1:443, and the v6 equivalent returns
[fd00::1]:8080 — confirming addr_eq_no_port is now called inside the
loop body in both helpers.
2026-04-27 21:29:23 -07:00
Pavel Punsky
301415d848 Unblock fuzz coverage for is_http and rare STUN attributes (#1884)
OSS-Fuzz introspector flags three blockers the fuzzer cannot reach on
its own:

1. findstr() in src/client/ns_turn_msg.c is gated by is_http(), which
requires GET/POST/PUT/DELETE prefix + " HTTP/" + "\r\n\r\n". The
fuzzer's binary STUN seeds never synthesize a valid HTTP frame.

2. stun_attr_get_reservation_token_value() and
stun_attr_get_response_port_str() are called from harness_attr_iter only
when the input contains the matching attribute type. Neither appears in
the existing seed corpus.

Add HTTP framing keywords to fuzzing/stun.dict and four new seed files
covering both gaps:

  - seed_http_get.raw: minimal "GET / HTTP/1.1\r\nHost: x\r\n\r\n"
- seed_http_post_clen.raw: POST with Content-Length to drive the strtoul
branch in is_http
- seed_reservation_token.raw: STUN allocate response with an 8-byte
RESERVATION-TOKEN attribute
- seed_response_port.raw: STUN binding request with a 4-byte
RESPONSE-PORT attribute

Each new STUN seed validated against the real parsers
(stun_get_message_len_str, stun_attr_get_first_by_type_str, is_http) to
confirm it reaches the targeted branch.

The corpus zips also drop pre-existing __MACOSX/ and .DS_Store entries
that had snuck in during a prior macOS zip step; net file count rises
(24 -> 28 in FuzzStun, 4 -> 8 in FuzzStunClient) while archive size
shrinks because of the junk removal.
2026-04-27 18:08:40 -07:00
Pavel Punsky
b4c138c409 Cover all public stun_buffer.c wrappers in FuzzStunClient (#1883)
Add harness_stun_buffer_api to FuzzStunClient.c that exercises every
public wrapper in src/apps/common/stun_buffer.c not already reached by
the existing harnesses: stun_get_size (NULL/non-NULL), the init_request
/ init_indication / init_success_response builders, the tid accessors,
the stun_is_indication wrapper (which gates the static is_channel_msg),
the attr_add / attr_add_channel_number / attr_add_addr /
attr_add_even_port (both branches) / attr_get_first_by_type accessors,
stun_set_allocate_request (rt NULL and non-NULL paths),
stun_set_binding_request /
stun_prepare_binding_request, and the channel-message wrappers.

Each builder call is followed by inspect_buffer_message so the resulting
serialized message is also walked by the parser predicates. A tail block
also pumps raw fuzzer bytes through the wrapper-form predicates
(stun_is_indication, stun_is_channel_message, stun_tid_from_message,
stun_attr_get_first_by_type) so they see malformed inputs the serializer
paths cannot produce.
2026-04-26 11:28:28 -07:00
Pavel Punsky
46e5117fb1 Extend fuzzing coverage and enable local fuzzing in a container (#1881) 2026-04-24 22:11:27 -07:00
Pavel Punsky
741b2983cc Extend STUN client fuzz builder coverage (#1878) 2026-04-22 19:06:41 -07:00
Pavel Punsky
c8b3dd6513 Merge 10 fuzz targets into FuzzStun and FuzzStunClient via dispatcher (#1873)
Upstream OSS-Fuzz build recipe
(google/oss-fuzz/projects/coturn/build.sh) only copies two fuzzer
binaries -- FuzzStun and FuzzStunClient -- and their seed corpora into
$OUT. The eight additional fuzz targets added later never ran on
oss-fuzz.com, which is why the introspector profile reports "fuzzer no
longer available" for them.

Rather than patching the Google-owned build recipe, fold all fuzzers
into the two binaries OSS-Fuzz actually ships. Each target now begins
with a single-byte selector (Data[0] mod 5) that dispatches to one of
five sub-harnesses:

  FuzzStun        - integrity (SHA1/multi-SHA), attr_iter, attr_add,
                    old_stun
  FuzzStunClient  - stun_client, channel_data, addr_codec, oauth_token,
                    oauth_roundtrip

No upstream OSS-Fuzz changes are required.
2026-04-19 13:00:19 -07:00
Pavel Punsky
42826e86dc Extend seed corpus (#1858) 2026-04-11 20:16:35 -07:00
Pavel Punsky
2342119db0 Add more fuzzing scenarios (#1857) 2026-04-11 18:02:08 -07:00
Pavel Punsky
46368b3e1e Fix stack buffer overflow in OAuth token decoding (#1850)
Fixes #1838
2026-04-06 19:25:54 -07:00
Pavel Punsky
c85248fb7a Fix uint16_t truncation overflow in stun_get_message_len_str() causes… (#1844)
… TCP channel framing bypass

stun_get_message_len_str (line 931) — widened bret from uint16_t to
uint32_t. 4 + 0xFFFF = 65539 now fits without truncation; the buffer
check bret <= blen correctly returns -1 when the full message hasn't
arrived.

stun_is_channel_message_str (lines 791–795) — replaced the silent
mutation of the caller's *blen with a local blen16 variable. The
original code wrote *blen = 65535 before the function even confirmed a
valid channel message, leaving the caller's buffer-length corrupted on a
false return.

Fixes #1837
2026-04-04 16:10:16 -07:00
Michael Jones
da332ed9e7 Add the InsertBraces command for clang-format to ensure that all conditionals always have braces (#1408)
- Why? Because code where conditionals lack braces is much harder to read, and prone to indentation confusion.
- How? Just added an extra flag to .clang-format and re-ran clang-format on all the files.

I also moved .clang-format up to the top level of the repo so that it can be applied to the fuzz targets as well.
2024-01-27 16:38:40 -08:00
Gustavo Garcia
d9108a4b54 Add clang format rules and checks (#935)
I would like to get feedback on this and see if people is confortable
with these clang rules.

Right now is using the "llvm" style increasing the line length from 80
to 120 given that coturn is using long lines often.

Co-authored-by: Pavel Punsky <eakraly@users.noreply.github.com>
2022-11-06 22:05:17 +01:00
Arjun
dda0c99759 fuzzing support (#982)
Adding fuzzing to finding memory-corruption-related bugs.

Hello coturn team,
Can you check this pr harness suite for creating harnesses and compiling
harnesses?
Any other thoughts on adding a new interface for fuzzing support ?


Signed-off-by: 0x34d <ajsinghyadav00@gmail.com>

Signed-off-by: 0x34d <ajsinghyadav00@gmail.com>
2022-10-24 22:01:58 +02:00