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

29 Commits

Author SHA1 Message Date
Pavel Punsky
1038f47bcc Deprecate option drop-invalid-packets (#2078)
This option has been on by default for multiple versions - removing the
flag completely
2026-09-07 13:44:29 -07:00
Pavel Punsky
285a9b76c9 Merge commit from fork
The UDP listener's stateless first-packet fast path short-circuits a first
datagram from an unknown source when the outcome does not depend on session
state, so it never allocates a child socket or a session. Its credential-mode
gate sat above the packet classification, so the short-circuit only applied in
long-term-credential mode; in no-auth mode the same first packets fell through
to create_ioa_socket_from_fd + open_client_connection_session and held that
state until the to-be-allocated watchdog expired, even though the relay never
acts on them.

Move the credential-mode gate below the classification: the decisions that do
not depend on credential type are made at the listener in every mode, and only
the 401/438 challenge synthesis stays long-term-only. A real request (e.g.
ALLOCATE) still takes the session path, and an error-producing request is still
reaped on the initial packet, so the bytes on the wire are unchanged.

Rename udp_stateless_nonce_fast_path to udp_stateless_fast_path to reflect the
mode-independent role, and document it in docs/stateless-nonce.md.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 17:47:26 -07:00
Pavel Punsky
c6cd2106b6 Enable stateless nonce by default (#2036)
## What

Makes `--stateless-nonce` the default, and closes two paths that created
a
per-packet session for unauthenticated UDP traffic even when it was
enabled.


## Behaviour changes for operators

1. **The default challenge nonce goes from 16 to 24 characters.** RFC
8489
requires clients to treat NONCE as an opaque string of up to 128
characters,
so compliant clients are unaffected. `--stateless-nonce=false` restores
the
   stored random nonce and the 16-character challenge.
2. **The default signing key is ephemeral**, so a restart costs each
client one
`438` re-auth round-trip, as does a retry that lands on a different
instance
behind a load balancer. `--stateless-nonce-secret` shares the key across
a
fleet and avoids both. (This is not a regression — today a retry landing
on
another instance finds no session either — but it becomes the default
story.)


## Validation

Run in a container (Linux, isolated network namespace):

- build clean; 17/17 unit tests
- 11 example suites with zero FAIL lines: `run_tests`, `_conf`,
  `_stateless_nonce`, `_mobile`, `_dtls_default`, `_multiplex_peer`,
  `_stateless_binding`, `_ratelimit_401`, `_mobility_quota`, `_expiry`,
  `_mobility_resume_flood`
- both fuzzing smoke targets (`ASan 0`, `ASan 1`)
- `clang-format-15` clean; mandoc lint unchanged at 104 pre-existing
warnings
2026-08-04 21:08:28 -07:00
Pavel Punsky
0c5ddc5481 Worktree stateless nonce mi fastpath (#2025)
What — the fast path fell through on the mere presence of
MESSAGE-INTEGRITY, so 20 garbage bytes put a spoofed flood back on the
session-creating path. Framed honestly as ~19 KB of alloc+zeroing per
~100-byte packet rather than an OOM, since close_after_auth_challenge
already bounded steady-state memory. Headline number: 200 forged-MI
packets → 200 sessions before, 0 after, all still answered.
How, part 1 — nonce validated in the listener; 400 / 437 / 441 / 438
answered there in check_stun_auth's order; wrong-length MI treated as
missing; a verifying nonce still falls through because the credential
lookup can be async. Plus the --stun-only side improvement.
How, part 2 — rejections charged to the existing
--unauthorized-ratelimit bucket, with the reasoning for not dropping
them: RFC 8489 §9.2.5 needs the 438 for stale-nonce recovery, and after
part 1 that legitimate retry lands on this very path. Includes the
amplification measurements (438 under 1:1 vs 401 at 3.6–5.4:1) to
explain why this caps a vector rather than closing an amplification
hole.
Wire compatibility — nine shapes dumped attribute-by-attribute against a
pre-change build, identical.
Tests — the new probe script and its assertions, the two rate-limit
cases, and the full green matrix including fuzz smoke and the Docker
image Bats run.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:57:23 -07:00
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
50339c9e11 docs: state OpenSSL 3.0+ as the supported target; drop stale guidance (#2011)
coturn already targets OpenSSL 3.x (the DH-param migration in #1809
dropped 1.1.1 build support, and RFC 7250 raw public keys need 3.2.1),
but several docs still referenced OpenSSL 1.0.0a/1.0.1/1.0.2 and 0.9.8
as if they were supported or recommended baselines.

Make the requirement explicit and consistent: OpenSSL 3.0 or newer is
the supported target; older releases (1.x, 1.1.x, 0.9.x) and other
libraries are not supported and are not tested or validated against.

- docs/OpenSSL.md: lead with the 3.0+ requirement and the
unsupported/not- validated statement; note the RFC 7250 raw-public-keys
3.2.1 sub-requirement and the deprecated patches/openssl-1.1.1 bridge;
reframe the custom-prefix build guidance for "system ships < 3.0"
instead of the old 1.0.1 example.
- docs/Build.md: replace the "1.0.0a or better recommended" /
0.9.8-DTLS-over- IPv6 paragraph with the 3.0+ requirement.
- README.md: mark the openssl dependency as 3.0 or newer.
- README.turnserver: drop the pre-1.0.2 EC-curve and 1.0.2-ALPN version
caveats (both moot at 3.0+).
2026-07-26 15:19:07 -07:00
Pavel Punsky
5fada2b30b docs: record the RFC 3489 deprecation roadmap; fix stale option text (#2009)
Follow-up to #2008. Documentation only — no functional change.

## Why

#2008 split `--stun-backward-compatibility` and moved the RFC 3489
("classic" STUN) request path behind a deprecated
`--rfc3489-compatibility`. But the two-step retirement plan lived only
in that PR's description: nothing in the tree said what gets removed,
when, or what an operator should do about it.

## What this adds

**`docs/rfc3489-deprecation.md`** — the reference for both audiences:

- **Why** classic STUN is being retired (the magic-cookie split, RFC
5389 → RFC 8489).
- **What each flag now controls**, plus the full address-attribute
matrix across all four combinations.
- **Migration**: which flag to set depending on *why* you originally set
the combined one; setting both reproduces the old semantics exactly.
- **"Do you still need it?"** — the exact log lines that reveal
classic-STUN clients, including the detail that `OLD BINDING` needs
*both* `--verbose` and `--log-binding`.
- **What is NOT affected** — `--rfc5780` NAT behavior discovery shares
the `CHANGE-REQUEST` attribute and is untouched; `MAPPED-ADDRESS` on
modern responses stays.
- **Removal roadmap** — step 1 (done) and step 2, with a per-file table
of what comes out and two constraints recorded up front:
- Keep `OLD_STUN_ATTRIBUTE_PASSWORD` (0x0007) — it is deliberately
skipped so a modern client that gratuitously includes it isn't answered
`420 Unknown Attribute`. Deleting that case turns tolerated-and-ignored
into a hard error.
- Expect an **API break**: the `old_stun_*` declarations ship as public
headers (`src/client/*.h` → `include/turn/client/`, installed beside
`lib/libturnclient.a`), which is why step 2 needs a major version.

## Also fixed

`README.turnserver` — the plain-text twin of the man page — still
carried the pre-split description:

> Enable handling old STUN Binding requests using MAPPED-ADDRESS
attribute in binding response (instead of XOR-MAPPED-ADDRESS).

That was stale on two counts even before #2008 (the attribute is added
*in addition to*, not *instead of*, `XOR-MAPPED-ADDRESS`), and it had no
entry for the new option. Rewritten to match the man page, with the
migration note.

The new doc is linked from the RFC 3489 bullet in `README.md` and from
item 22 in `STATUS.md`.

## Checks

Every file path and symbol referenced by the new doc was verified to
exist on current master. The behavior matrix is the one validated
end-to-end in #2008. No C files touched, so the `clang-format` gate and
build are unaffected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:20:42 -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
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
ce8b25737d feat: RFC 8016 graceful dual-5-tuple mobility handoff (#1975)
Historically a MOBILITY-TICKET resume hard-switched the allocation onto
the new client socket at REFRESH time and closed the old socket
immediately. RFC 8016 instead requires the server to keep receiving on,
and transmitting peer->client data to, the old 5-tuple until the client
is active on the new one, and only then discard the old 5-tuple.

Implement that make-before-break handoff:

- handle_turn_refresh no longer transfers the socket. It rotates the
mobility ticket, refreshes the allocation on the original session, and
opens a bounded transition: the original session keeps the allocation
and its old socket (peer->client egress stays on the old 5-tuple), while
the resuming session is kept alive and linked as "pending" and the
REFRESH success response is sent on the new socket.
- read_client_connection promotes the allocation onto the new socket on
the client's first packet from the new 5-tuple
(mobile_complete_transition): the socket is moved to the allocation
session, which closes the old socket and discards the old 5-tuple, and
the packet is then served by the allocation. RFC names the trigger as
the first Send/ChannelData; we promote on any first packet so control
requests (CreatePermission/ChannelBind/Refresh) a roaming client issues
from the new address are served by the allocation.
- The per-session sweep aborts a transition left open past a 30s
deadline, conservatively keeping the allocation on the old path and
reaping the pending session. shutdown_client_connection unlinks a
transition partner on teardown.

Transition state (session ids + deadline) is added to
ts_ur_super_session; the pending session's un-allocated watchdog is
disarmed and its stray mobile-map entry removed while it legitimately
holds no allocation.

Also align resume error codes with RFC 8016: an unusable/unknown ticket
now returns 437 (Allocation Mismatch) instead of the non-standard 404.

examples/run_tests_mobile.sh now asserts, from the server log, that the
handoff actually executed (mobile_complete_transition emits a marker),
so a regression that skipped or broke the transition fails the test.
docs/mobility-rfc8016.md documents the design, the trigger/deadline
semantics, the quota note, and the deferred ticket-entropy hardening
item.
2026-07-14 21:28:29 -10:00
Pavel Punsky
47ce433cde Add per-source rate-limiting of UDP 401 Unauthorized responses (#1957)
TURN/STUN long-term auth answers the first unauthenticated request with
a 401 Unauthorized carrying REALM/NONCE. On UDP, where source addresses
are spoofable and the 401 is larger than the request that triggers it,
the server becomes a reflection/amplification vector: an attacker forges
a victim's source IP and bounces amplified 401s at it.

This adds an opt-in mitigation that caps how many UDP 401 responses the
server emits toward any single source IP per second. Past the cap the
server stays silent, denying both the reflection and the amplification.
Authenticated relay traffic is untouched — only requests that reach the
401 branch spend budget.

Flags (off by default):
  --unauthorized-ratelimit            Enable the mitigation (UDP only).
--unauthorized-ratelimit-rps=<n> Max 401 responses per source IP per
second. Default 10.

Implementation:
- Lock-free, fixed 4096-bucket direct-mapped table (~80 KiB, no
malloc/free, no lock, no list scan on the hot path); the source IP
appears only as a 32-bit tag, never as a pointer. The port is stripped
from the key so an attacker cannot evade the limit by rotating the
source port. A fixed 1-second window backs the per-second limit.
- Colliding addresses share the live bucket budget instead of receiving
a fresh allowance, so hash collisions can never expand reflected output.
- Drop and collision diagnostics are each latched to one log line per
(bucket, window) to avoid log amplification.
- Portable 32-bit atomics over C11 <stdatomic.h> with an MSVC
Interlocked* fallback; the on/off flag and the per-second threshold are
pointers into turn_params so every relay thread sees live CLI values
without per-thread copies.

Observability (Prometheus):
- turn_unauthenticated_401_{requests,responses,dropped_responses}
- turn_ratelimit_{hash_collisions,occupied_buckets,total_buckets}

Tests & docs:
- Unity unit test (tests/test_ratelimit.c) covering live-bucket
collisions, occupancy tracking, and the collision counter.
- End-to-end positive/negative system test (run_tests_ratelimit_401.sh)
and a Prometheus counter test (run_tests_prom.sh).
- A 401-response-flood load harness under examples/loadtest/.
- docs/401-ratelimit.md design note, plus man page, README, Prometheus
docs, and the example/Docker turnserver.conf entries.
2026-06-21 14:25:09 -07:00
Pavel Punsky
53f3ed9104 Prom https (#1956)
## Summary

Adds an optional **HTTPS mode for the Prometheus `/metrics` endpoint**,
and bundles three supporting improvements that surfaced while building
and testing it: more reliable `libmicrohttpd` detection, a fix for slow
`turnserver` startup on macOS, and expanded/robust Prometheus tests.

## Changes

### 1. Optional HTTPS for the Prometheus exporter
The exporter previously served `/metrics` over plain HTTP only. It can
now optionally serve over HTTPS via libmicrohttpd's TLS support.

- `--prometheus-tls` — serve the metrics endpoint over HTTPS instead of
HTTP.
- `--prometheus-cert <file>` / `--prometheus-key <file>` — PEM cert/key
for the endpoint; default to the server's own `--cert` / `--pkey` when
unset.

`start_prometheus_server()` loads the cert/key into memory, enables
`MHD_USE_TLS`, and passes `MHD_OPTION_HTTPS_MEM_CERT/KEY`. If TLS is
requested but no cert/key can be loaded (or libmicrohttpd lacks TLS
support), it logs an error and does not start. The startup log now notes
the `(http)`/`(https)` scheme. `docs/Prometheus.md` documents the new
flags.

*(`src/apps/relay/prom_server.c`, `mainrelay.c`, `mainrelay.h`,
`docs/Prometheus.md`)*

### 2. Reliable `libmicrohttpd` detection in `configure`
The autotools `configure` probed libmicrohttpd with a bare `cc
-lmicrohttpd` link test (no include/lib paths), so it failed to find
Homebrew-installed libmicrohttpd on macOS and silently disabled
Prometheus — even though OpenSSL/libevent (also Homebrew) were found via
pkg-config. `configure` now detects libmicrohttpd via `pkg-config` first
(matching how libssl/libevent are already detected), falling back to the
bare link probe on platforms without a `.pc` file. `./configure && make`
now builds the exporter on macOS without manual `CFLAGS`/`LDFLAGS`.

*(`configure`)*

### 3. Faster `turnserver` startup on macOS
macOS lacks `pthread_barrier_*`, so the thread-startup rendezvous in
`netengine.c` degraded to a flat `sleep(5)` — adding ~5s to every
startup (and it was racy: threads proceeded even if setup took longer
than the fixed sleep). Replaced the shim with a real portable barrier
built on a mutex + condition variable, behind a unified macro API:
**native `pthread_barrier` is still used on Linux**, the portable
barrier is used only where it is unavailable. The previous racy
`sched_yield()` spin-wait fallback is removed as well.

*(`src/apps/relay/netengine.c`)*

### 4. Prometheus test coverage + robustness
`examples/run_tests_prom.sh` now also covers the HTTPS endpoint
(explicit cert/key, cert/key inherited from `--cert`/`--pkey`, and a
negative test that an unreadable cert keeps the endpoint down, including
a check that the TLS endpoint rejects plaintext HTTP). Readiness is now
detected by polling the endpoint / server log instead of fixed `sleep`
intervals, making the suite both faster (~70s → ~2s) and less flaky.

*(`examples/run_tests_prom.sh`)*

## New flags

| Flag | Description |
|---|---|
| `--prometheus-tls` | Serve `/metrics` over HTTPS. Requires
libmicrohttpd built with TLS support. |
| `--prometheus-cert <file>` | PEM certificate for the HTTPS endpoint
(default: the server's `--cert`). |
| `--prometheus-key <file>` | PEM private key for the HTTPS endpoint
(default: the server's `--pkey`). |

## Validation

- **macOS** (portable-barrier path): `clang-format` clean, 10/10 unit
tests, and `run_tests.sh` / `run_tests_conf.sh` /
`run_tests_multiplex_peer.sh` / `run_tests_prom.sh` all pass. Exporter
now reachable at startup `t=0` (was `t=5`).
- **Linux** (Docker, native-`pthread_barrier` path): clean build, 9/9
unit tests, and all four system-test suites pass (TCP/TLS/UDP/DTLS).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 23:02:01 -07:00
Pavel Punsky
1ec81eff10 Build Prometheus exporter from vendored local sources (#1955)
## Summary

The Prometheus exporter previously linked the
[digitalocean/prometheus-client-c](https://github.com/digitalocean/prometheus-client-c)
(`prom`) library, which is **no longer maintained**. This replaces it
with a minimal, self-contained Prometheus client under `src/prometheus/`
that is built straight from coturn's own sources and implements exactly
the slice of the API coturn uses: counters, gauges, a single default
registry, and the text-exposition serializer.

The vendored client is **drop-in API-compatible**, so `prom_server.c` is
unchanged. Histograms, summaries, custom registries and the upstream
`promhttp` handler are intentionally omitted (coturn serves `/metrics`
with its own libmicrohttpd handler).

The exporter is now **always compiled in**; the runtime `--prometheus`
flag still controls whether it starts. **libmicrohttpd** — actively
maintained — is the only remaining external dependency and is now
required by the build.

## Changes

- **`src/prometheus/{prom.h,prom.c}`** — vendored client. Thread-safe
(per-metric mutex on the increment path, read under lock in the bridge),
label-aware, with proper Prometheus value/label escaping and clean
integer formatting for counters. No deps beyond pthread/libc.
- **CMake** — replace `find_package(Prometheus)` with required
`find_package(MicroHTTPD)`; add `cmake/FindMicroHTTPD.cmake`; remove
dead `cmake/FindPrometheus.cmake` and the `WITH_PROMETHEUS` option.
- **`configure` / `Makefile.in`** — drop `-lprom`, compile the vendored
source.
- **Docker** (`debian`, `alpine`) and **CI** (`ubuntu-build-deps`,
`mingw`) — remove the `prometheus-client-c` build/install steps; keep
libmicrohttpd.
- **`docs/Prometheus.md`** rewritten.
- **`tests/test_prometheus.c`** — Unity tests for counter
accumulation/monotonicity, gauge inc/dec, and label-value escaping.

## Validation

- Full build green; all unit tests pass (incl. new `test_prometheus`);
`clang-format-15` clean.
- Live exporter scraped over HTTP — all `run_tests_prom.sh` scenarios
(default / custom address / port / path, plus the negative case) pass.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:58:30 -07:00
Pavel Punsky
273f2e8a0c Add optional TLS transport for Redis connections (#1951)
The Redis driver previously connected only over plaintext TCP, so a
Redis server configured for TLS-only (tls-port) was unreachable. Add an
opt-in TLS transport built on the hiredis_ssl library, covering both the
synchronous userdb path and the asynchronous statsdb path.

Fixes #651

New connection-string keys (for --redis-userdb and --redis-statsdb):
tls/ssl, ca/cacert, capath, cert, clientkey, sni/servername, verify
(peer|none). Server-certificate verification is on by default.

- Extract the connection-string parser into a dependency-free unit
(dbd_redis_conninfo.c/.h) and unit-test it
(tests/test_redis_conninfo.c).
- hiredis_ssl is detected via pkg-config; without it tls=true is
rejected with an explicit error and the build still compiles
(hiredis-only).
- The blocking sync handshake honors connect_timeout so a plaintext
endpoint mistakenly addressed with tls=true cannot hang.
- Document the options in docs/Redis.md, the man page, and example
config.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:04:29 -07:00
Pavel Punsky
8c7d8fcb86 Enable --udp-recvmmsg by default on Linux (#1930)
## Summary

Flips the Linux default for `--udp-recvmmsg` from **off** to **on**.
Operators opt out with `--udp-recvmmsg=false` (or `=0`).

> **Stacked on #1929.** This depends on the recvmmsg-scoping change in
#1929 and is based on that branch, so the diff shows only the default-on
change. GitHub will auto-retarget the base to `master` once #1929
merges. Merge #1929 first.

## Why this is now safe

The original objection to default-on (recorded in
`docs/PerformanceIterationLog.md`) was the **per-session-relay-socket
prealloc tax**: `--udp-recvmmsg` applied the 16-buffer batch path to
every connected relay socket, which only ever carries one flow, so the
churn ate the listener-side win.

#1929 scoped recvmmsg to **shared fan-in sockets only**
(`udp_recvmmsg_eligible`: the client listener, plus the per-thread
shared relay socket under `--multiplex-peer`). Per-session relay sockets
now stay on the single-recv path regardless of the flag, so that tax is
gone. The one socket touched by default — the client listener — is a
genuine fan-in point:

- batches whenever client concurrency is non-trivial (measured
`avg_batch ≈ 16` under load), and
- costs little when idle (few packets ⇒ few prealloc cycles).

## What changed

- `mainrelay.c`: `turn_params.udp_recvmmsg` default `false → true`
(Linux only).
- Removed the now-dead `--multiplex-peer` auto-enable block and the
`udp_recvmmsg_set_explicitly` tracking it relied on; multiplex-peer gets
its recvmmsg window from the default. The opt-out flows through the
normal `get_bool_value` path.
- Help text, `man/man1/turnserver.1`, `examples/etc/turnserver.conf`,
`CLAUDE.md`, and `docs/PerformanceIterationLog.md` updated for the new
default + opt-out.

Per-session relay sockets and DTLS session sockets are unchanged.

## Validation

- **Format:** clang-format 15.0.7 clean.
- **macOS:** build + ctest 6/6 + `run_tests.sh` pass.
- **Linux (Docker, clean build):** ctest 5/5; `run_tests.sh`,
`run_tests_conf.sh`, `run_tests_multiplex_peer.sh` all pass (no FAIL).
- **Runtime proof (loopback, `--udp-recvmmsg-log`):**
- Default, no flag: recvmmsg active, `calls=13714 packets=219306
avg_batch=15.99`.
  - `--udp-recvmmsg=false`: zero recvmmsg activity — opt-out confirmed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 21:27:05 -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
Pavel Punsky
5959ecfb13 Add UDP-GSO send path (--udp-gso) (#1907)
## Summary

- New `--udp-gso` flag (Linux, requires `--udp-sendmmsg`) collapses
same-destination, same-size sendmmsg batches into a single `sendmsg`
with a `UDP_SEGMENT` cmsg, so the kernel allocates one super-skb that
traverses the network stack once and is segmented at egress instead of
running `udp_sendmsg → ip_finish_output → __dev_queue_xmit` per
datagram.
- Also wraps the relay-side `recvmmsg` callback loop in
`udp_sendmmsg_batch_begin/end` so peer→client sends triggered inside a
recv batch can also coalesce — without that wrapping the relay path
issues one `sendto` per delivered datagram.
- Sticky-disable on `EINVAL/ENOPROTOOPT` for older kernels/NICs that
lack UDP-GSO; one warning logged, then transparent fallback to the
existing `sendmmsg` and `udp_send` paths.

## Why

The `--udp-recvmmsg` and `--udp-sendmmsg` follow-ups confirmed (see
[docs/PerformanceIterationLog.md](docs/PerformanceIterationLog.md)) that
on the relay flood workload the dominant cost is the per-datagram kernel
TX path. mmsg-style batching reduces only the syscall entry/exit, not
the per-skb stack traversal — UDP-GSO collapses both.

## Result

DigitalOcean nyc1 c-4, 30 s alternating A/B, `-Y packet -m 1`, eth1 TX
as the authoritative server forwarding metric:

| Variant | eth1 RX | eth1 TX | sys CPU | idle CPU |
|---|---:|---:|---:|---:|
| baseline (no flags) | 322,091 | 127,445 | 22.9 % | 67.5 % |
| `--udp-recvmmsg --udp-sendmmsg --udp-gso` | 266,068 | **257,996** |
15.0 % | 78.7 % |
| baseline (no flags) | 309,475 | 125,573 | 20.9 % | 70.7 % |
| `--udp-recvmmsg --udp-sendmmsg --udp-gso` | 275,992 | **225,366** |
14.9 % | 74.3 % |

Mean server forwarding rate: **126.5 k → 241.7 k pps (+91 %, 1.91×)**,
mean system CPU **21.9 % → 14.9 %** — about **2.8× CPU efficiency** (TX
pps per system-CPU-%). Full perf-children comparison and methodology in
the new section of
[docs/PerformanceIterationLog.md](docs/PerformanceIterationLog.md).

## Notes for reviewers

- `--udp-gso` is opt-in and requires `--udp-sendmmsg` (the help text
states the dependency). Without `--udp-sendmmsg` the batch state never
accumulates and GSO has nothing to flush.
- GSO eligibility resets on every `_begin/_end`. Mixed-destination,
mixed-size, or oversize batches transparently fall back through
`sendmmsg` / `udp_send`.
- Rebased onto current `master`; the recvmmsg dependency is already
merged via #1906.

## Test plan

- [x] `cmake --build build --target turnserver` (RelWithDebInfo + ASan
local builds clean)
- [x] `ctest --test-dir build --output-on-failure` — 3/3 unit tests pass
- [x] `examples/run_tests.sh` — TCP/TLS/UDP pass; DTLS pre-existing
failure on macOS environment, unrelated to this change
- [x] DigitalOcean A/B perf validation captured above
- [ ] Reviewer to confirm CI green on Linux build/test/CodeQL

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 08:05:38 -07:00
Pavel Punsky
a5005c4193 Relay recvmmsg (#1906)
## Summary

Extends the existing Linux-only `--udp-recvmmsg` flag from the UDP
listener socket to also cover **connected per-session UDP relay
sockets**, so steady-state client→relay and peer→relay traffic on plain
UDP is read in batches of up to 16 datagrams per `recvmmsg(2)` instead
of one `recvmsg` per packet. DTLS sessions still go through the SSL read
path and are unchanged.

The flag stays **opt-in**: receive-side batching works correctly, but on
the current `m=1` / `m=100` benchmarks throughput is flat to slightly
negative — the bottleneck has moved past receive (see results below).

## What's in the change

- **Shared receive helpers** (`src/apps/relay/ns_ioalib_engine_impl.c`,
`src/apps/relay/ns_ioalib_impl.h`):
- `ioa_parse_udp_recvmsg_cmsg()` — single TTL/TOS/`IP_RECVERR` cmsg
parser used by both `udp_recvfrom()` and the new batch path. Replaces
the duplicated parser previously inlined in `dtls_listener.c` and
`udp_recvfrom()`.
- `ioa_init_recvmmsg_hdr()` — single initializer for
`mmsghdr`/`iovec`/cmsg/source-address fields, also used by the listener.
- New `IOA_UDP_RECVMMSG_MAX_BATCH = 16` constant; both listener and
relay paths now share it.
- **Connected relay batch read** (`socket_udp_read_batch_recvmmsg` in
`ns_ioalib_engine_impl.c`): called from `socket_input_worker` for
non-SSL UDP sockets when `--udp-recvmmsg` is on. Allocates per-message
`stun_buffer_list_elem`s, calls `recvmmsg(MSG_DONTWAIT)`, dispatches
each datagram through the existing `read_cb` path, and falls back
cleanly on `ENOSYS`/`EINVAL`/`EOPNOTSUPP` (auto-disables the flag) and
on `EAGAIN`/short-batch (releases unused buffers).
- **Per-engine scratch state**: the `mmsghdr[16]` / `iovec[16]` / cmsg /
src-addr arrays live on `ioa_engine`, not on every socket — keeps memory
flat at thousands of allocations.
- **TTL/TOS-sized cmsg buffers** in the listener: the listener
previously over-allocated `64 KiB` per slot; it now uses the same
TTL+TOS sizing as the relay path.
- **Opt-in occupancy stats** behind a new `--udp-recvmmsg-log` flag:
every 10 s the relay logs `udp-recvmmsg stats: calls=… packets=…
avg_batch=… wouldblock=… unavailable=… no_buffer=… hist_1=… hist_2=…
hist_3_4=… hist_5_8=… hist_9_16=…`. Counters are always tracked (cheap);
the periodic log is gated by the new flag so default operation is
silent.
- **CLI plumbing**: `--udp-recvmmsg-log` long option in
`mainrelay.c`/`mainrelay.h`, `cli_print_flag` entry in
`turn_admin_server.c`, doc updates in `README.turnserver`.
- **Docs**: `docs/PerformanceIterationLog.md` records the iteration
steps, validation, and two rounds of DigitalOcean A/B numbers.
`CLAUDE.md` load-test instructions updated to mention the new flag and
the `tot_recv_msgs` / `tot_recv_bytes` workaround.
2026-05-08 22:47:46 -07:00
Pavel Punsky
69bc0e7351 Load generator mode in turnutils_uclient (#1894)
## Summary

Adds load-generator modes to `turnutils_uclient` for repeatable TURN
server performance testing:

- Adds `-Y packet|alloc|invalid` load modes.
- Supports packet flood, allocation flood, and invalid-packet flood
workflows.
- Adds unique local client ports for allocation flood mode.
- Removes default packet pacing in load-generator modes unless
explicitly set.
- Adds helper scripts under `examples/loadtest/`.
- Documents load-test usage in `README.turnutils`,
`man/man1/turnutils.1`, `CLAUDE.md`, and
`docs/PerformanceIterationLog.md`.

The performance log captures DigitalOcean benchmark methodology, A/B
lessons, hot-path findings, and future optimization candidates.
2026-05-03 22:03:08 -07:00
Pavel Punsky
69aa0e4401 Keep only NEV_UDP_SOCKET_PER_THREAD network engine (#1849)
Remove the two engine implementations (NEV_UDP_SOCKET_PER_SESSION and
NEV_UDP_SOCKET_PER_ENDPOINT) and all the dispatch/selection logic around
them. NEV_UDP_SOCKET_PER_THREAD is now the sole, unconditional
implementation.

- mainrelay.h: removed _NET_ENG_VERSION enum, typedef, and
net_engine_version / net_engine_version_txt struct fields
- mainrelay.c: removed NE_TYPE_OPT CLI option, set_network_engine(),
per-endpoint branch in print_features(), and all remaining
net_engine_version references
- netengine.c: removed run_udp_listener_thread(),
setup_socket_per_endpoint_udp_listener_servers() (~190 lines),
setup_socket_per_session_udp_listener_servers() (~90 lines); simplified
setup_barriers(), setup_relay_server(), run_general_relay_thread(),
setup_general_relay_servers(), and setup_server() by eliminating all
engine-type conditionals
- turn_admin_server.c: replaced dynamic engine version lookups with
hardcoded values (3 / "UDP thread per CPU core") in CLI and HTTPS status
handlers

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:26:46 -07:00
Captain Picarl
a6028ddd8a Improve PostgreSQL.md Clarity (#1833)
A rewrite and reformat of the PostgreSQL.md file:
- Applies some minor formatting to the document. 
- Applies consistency between the 'turn' user and 'coturn' database
names.
- Adds some warnings about issues I encountered during my own
installation process (public schema error)
- Adds a minor example of restarting a service with SystemD
2026-03-12 20:53:40 -07:00
Gustavo Garcia
ce56a78a7a Add modules diagram to developer doc (#1685)
Add basic diagram with modules of coturn inside Developer.md.

I also wanted to move the existing Flow chart to mermaid so that our doc
doesn't depend on external tools but i couldn't find the time to do it
properly (horizontal arrows are hard to represent with mermaid) so i
postponed that part.

This is the result of the mermaid diagram: 
![Screenshot 2025-05-26 at 12 54
29](https://github.com/user-attachments/assets/860bd56b-cb4a-462c-b931-a82c9d52fdc9)
2025-05-26 13:57:13 +02:00
dominiquefournier
460cfa38af Update SQLite.md (#1429)
Markdown is now proper
2024-02-09 20:14:49 -08:00
Kang Lin
d33112f84a Update FlowChart (#1377) 2024-01-16 19:54:19 -08:00
Kang Lin
05900adb2e Doc: add flowchart (#1328)
The flowchart will be kept updated:

- Use [drawio](https://app.diagrams.net/) to edit.

1. Open [drawio](https://app.diagrams.net/) in brower
2. Menu → File → Open from ... → Device:

Select [FlowChart.html](FlowChart.html)

3. Edit flow chart
4. Export to svg:

Menu → File → Export as... → SVG...
2024-01-15 19:14:21 -08:00
vuittont60
da6bc6b0c4 Fix typos (#1345) 2024-01-15 18:31:16 -08:00
Kang Lin
294f897ccd CMake: find prometheus(fix #1304) (#1315)
fix #1304
2023-11-22 20:34:53 -08:00
Pavel Punsky
85c11b69e3 Move and split documentation files (#1096)
This is not a long term documentation works - this is a temp proposal to
modernize existing documentation

- Create docs folder
- Move existing documentation files into docs
- Split one massive INSTALL files into smaller files (no text editing
was done except small markdown header formatting)
- Rename files to be markdowns for nicer rendering
- Bad md rendering is expected in many places
2022-12-22 11:13:24 -08:00