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

138 Commits

Author SHA1 Message Date
Pavel Punsky
ac601f832d key OAuth MESSAGE-INTEGRITY with the whole mac_key (#2096)
RFC 7635 Section 6.2 keys HMAC-SHA1 MESSAGE-INTEGRITY with the token's
full 160-bit mac_key. The OAuth path went through the
long-term-credential helpers, which hash only get_hmackey_size(SHA1) =
16 bytes, so requests from RFC 7635 compliant clients failed with 401
and coturn's responses could not be verified by them. turnutils_uclient
shared the truncation, so the bundled client never noticed.

Add integrity helpers that take an explicit key length and use the whole
mac_key for OAuth sessions, both when checking requests (userdb and
check_stun_auth) and when signing responses. --oauth-legacy-mac-key on
turnserver (and on turnutils_uclient) restores the 16-byte truncation
for deployments that depend on it; the server logs a warning when it is
on. A mac_key whose last 4 bytes are zero hashes the same in both modes.

Fixes #2091 and #644

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:59:49 -07:00
Pavel Punsky
36b2f14516 Update help with missing arguments (#2079)
## Summary
- Remove the deprecated `--no-dtls` and `--no-cli` turnserver flags —
both were already no-ops (DTLS and CLI are off unless `--dtls`/`--cli`
is passed), so the flags only added deprecation-warning noise.
- Update `README.turnserver` and the `Usage` help text in `mainrelay.c`
to drop the removed flags.
- Strip now-unused `--no-dtls`/`--no-cli` invocations from the example
test scripts and update `run_tests_dtls_default.sh` to drop the coverage
for the deprecated flag's warning/compat behavior.
- Update `CLAUDE.md`'s test-suite notes and load-test snippet to match.

## Test plan
- [ ] `cmake --build build && ctest --test-dir build
--output-on-failure`
- [ ] `cd examples && ./run_tests.sh && ./run_tests_conf.sh &&
./run_tests_dtls_default.sh`

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-09-07 15:43:30 -07:00
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
1e3570dd1e Advertise IPv6 allocations under their real relayed transport address (#2058)
Fixes #1322.

The issue reports `clnet_allocate: !!!: relay addr cannot be received
(2)` when asking `turnutils_uclient` for an IPv6 relay. It turns out to
be two independent bugs that both end in that same line, one on each
side of the wire. Both are reproduced by the new test, which fails on
`master` and passes here.

## What the RFC requires

RFC 8656 Section 3 defines the relayed transport address as *"A
transport address on the TURN server that is used for relaying packets
between the client and a peer"*, and Section 7.2 leaves the server two
outcomes for an Allocate carrying `REQUESTED-ADDRESS-FAMILY`:

> If the server does not support the address family requested by the
client in REQUESTED-ADDRESS-FAMILY, or if the allocation of the
requested address family is disabled by local policy, it MUST generate
an Allocate error response, and it MUST include an ERROR-CODE attribute
with the 440 (Address Family not Supported) response code.

Section 7.3 puts one duty on the client:

> If the client receives an Allocate success response, then it MUST
check that the mapped address and the relayed transport address or
addresses are part of an address family or families that the client
understands and is prepared to handle.

## 1. `--external-ip` was applied across address families (server)

`external_ip` is a single `ioa_addr` and `--external-ip` may only be
given once, but it was substituted for the relayed address of *both*
families. On a server with an IPv4 `--external-ip`, an IPv6 allocation
was therefore advertised under an unrelated IPv4 address that no peer
can reach — neither an IPv6 relay nor a 440.

```
--relay-ip=127.0.0.1 --relay-ip=::1 --external-ip=127.0.0.1, client over ::1 with -x
INFO IPv4. Received relay addr: 127.0.0.1:30654
INFO clnet_allocate: !!!: relay addr cannot be received (2)
```

The relay socket really was IPv6; only the advertised address was wrong.
As a second effect, the substituted family made the response draw
`LIFETIME` from the IPv4 relay session that was never created for that
allocation.

`turn_set_xor_relayed_addr()` now applies the external IP only to a
relay of its own family, which is the same rule the
`ioa_addr_add_mapping()` setup path already follows in `mainrelay.c`.
The option's documentation is updated to match.

## 2. `turnutils_uclient` rejected an IPv6 relay it can handle (client)

The acceptance test took an IPv6 relayed address only when IPv6 had been
requested *explicitly*, and consulted the global
`default_address_family` rather than the `af` argument the caller passed
to `clnet_allocate()`. Against a server running
`--allocation-default-address-family=keep` (or the deprecated
`--keep-address-family`), which answers an IPv6 client with an IPv6
relay and no `REQUESTED-ADDRESS-FAMILY` in play, a perfectly usable
relay was refused — as reported in the issue thread. Section 7.3 asks
only that the client refuse a family it *cannot handle*, so the check
now rejects solely on a contradiction with the family this allocation
requested.

Note this does not make `keep` conformant — it is a deliberate
deviation, and `turnserver -h` says as much (*"It breaks RFC6156
section-4.2 (violates default IPv4)"*). The client simply stops
rejecting a relay it is able to use.

## Testing

`examples/run_tests_ipv6_relay.sh` covers both configurations end to
end, asserting a completed relayed round trip rather than just a
successful allocate. It uses ports off the defaults the other scripts
share, and SKIPs where no IPv6 loopback is bindable. Wired into the
clang, cmake, linux and macos workflows.

Verified red before green: reverting only `src/` and rebuilding turns
both cases into failures.

- macOS: build + `ctest` 18/18; Linux container: build + `ctest` 17/17
- Linux: `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`, `run_tests_ipv6_relay.sh`,
`run_tests_dtls_default.sh` — all OK
- Fuzzing smoke tests (ASan targets 0 and 1) — OK
- Packaged Docker image (debian, arm64) + bats — 19 tests, 0 failures, 2
skipped

## Deliberately not in this PR

- The dual-allocation `508` guard when `external_ip_set` is left as is;
with family-aware rewriting it could now be relaxed to allow a
v4-rewritten + v6-as-is pair, but that is a separate behavioral change.
- `--external-ip` stays single-family. A per-family form would be the
complete answer for IPv6-NAT deployments.

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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:52:18 -07:00
Pavel Punsky
ce26268da8 Start the DTLS listeners only when --dtls is given (#2026)
## What

The DTLS client listeners are no longer started by default. They come up
only
when `--dtls` is given.

Until now DTLS came up automatically whenever usable certificates were
configured, so every deployment that wanted TLS also exposed a DTLS
handshake
path it had never asked for. That path is the most expensive
unauthenticated
entry point the server has: it builds an `SSL` object, an `ioa_socket`
and a
session *before* the DTLS cookie is verified. Deployments that do not
serve
DTLS clients were paying for that surface silently.

## Compatibility

`--no-dtls` keeps its meaning, including `--no-dtls=false` to ask for
DTLS, so
an existing configuration resolves to exactly the listeners it asked
for. Both
spellings now log a deprecation warning naming `--dtls`:

```
CONFIG: --no-dtls is deprecated and now redundant: DTLS listeners are not started unless --dtls is given
CONFIG: --no-dtls=false is deprecated: use --dtls to start the DTLS listeners
```

**This is a behaviour change for anyone who relied on the old default.**
A
deployment that configured certificates and passed no DTLS flag loses
its DTLS
listeners on upgrade. The only signal such an operator gets is a startup
line,
emitted when the certificates are usable but DTLS is off:

```
DTLS listeners are not started; use --dtls to start them
```

If the project would rather have a transition release — default
unchanged, but
logging that it is about to change — that is a small edit to this
branch; say
so in review.

## How

`turn_params` carries the state as `dtls` rather than the previous
`no_dtls`,
so the listener and netengine tests read as "DTLS is on" instead of
double
negatives, and `openssl_setup()`'s two-flag guards lose their De Morgan
wrapper (`!(no_tls && no_dtls)` becomes `!no_tls || dtls`). The telnet
CLI and
web admin listings consequently report `dtls` instead of `no-dtls`,
matching
the flag an operator actually sets.

Documentation follows in `README.md`, `README.turnserver`, the built-in
help
and `examples/etc/turnserver.conf`. The `--pkey` help entry had a
sentence
truncated mid-clause; the rewrite fixes it.

## Tests

New `examples/run_tests_dtls_default.sh` pins four cases:

- no flag: no DTLS context, the hint line is logged, and a
`turnutils_uclient -S`
  session cannot complete;
- `--dtls`: context created, DTLS client relays end to end;
- `--no-dtls`: warns, stays off;
- `--no-dtls=false`: warns, starts DTLS, client relays.

It is wired into all four workflows, so this default is now covered on
both
Linux and macOS. Example scripts that only ever wanted DTLS off drop the
now-redundant flag; the ones that drive DTLS clients pass `--dtls`.

Also run green: `ctest` (18/18 macOS, 17/17 Linux) and, on both
platforms where
applicable, `run_tests.sh`, `run_tests_conf.sh`,
`run_tests_multiplex_peer.sh`,
`run_tests_stateless_nonce.sh`, `run_tests_stateless_binding.sh`,
`run_tests_ratelimit_401.sh`, `run_tests_mobile.sh`,
`run_tests_rfc5780.sh`,
`run_tests_expiry.sh`; fuzzing smoke tests (ASan, both targets); and the
packaged Debian image build plus its Bats suite (19 tests, 0 failures,
2 skipped).

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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:39:55 -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
ca87ff0511 Add --drain-min-allocations: shutdown threshold for drain mode (#1997)
## Summary

In drain mode (entered on SIGUSR1) the server waits for the active
allocation count to reach exactly 0 before shutting down. This PR adds
`--drain-min-allocations=<number>` (also usable as a `turnserver.conf`
key) so drain completes once the active allocation count drops **to the
configured value or below**. The default of 0 preserves the existing
behavior exactly.

This is useful when a small number of long-lived allocations shouldn't
hold up a rolling restart or decommission indefinitely.

## Changes

- `turn_params_t` gains a `drain_min_allocations` field (default 0),
wired through the option enum, long-options table, usage text, and
config parsing. Negative values are logged and clamped to 0.
- The drain check in `run_listener_server` becomes
`global_allocation_count <= turn_params.drain_min_allocations`, and the
"Drain complete" log line now reports the remaining count and threshold.
- Documentation added to the example `turnserver.conf`,
`README.turnserver`, and the `turnserver.1` man page (man page edited in
place to match the committed txt2man output rather than regenerating, to
avoid unrelated formatting churn).

## Validation

- Functional smoke test: with `--drain-min-allocations=10` and 6 live
allocations, SIGUSR1 shut the server down, logging `Drain complete (6
allocations remaining, threshold 10)`. Control run with the default kept
draining while allocations were active. Config-file spelling verified.
- macOS: build, all 15 unit tests, `run_tests.sh`, `run_tests_conf.sh`,
`run_tests_mobile.sh`, `run_tests_multiplex_peer.sh`,
`run_tests_rfc5780.sh` — all pass, no FAIL lines; clang-format clean.
- Fuzzing smoke tests (ASan, both targets): pass.
- Linux (Docker): clean build, ctest, all five system-test suites pass.
- Packaged Debian Docker image (arm64): builds; Bats tests 19/19 pass (2
environment skips).
2026-07-21 22:08:49 -07:00
Cybermilitia
89ee928358 Alternate server tcp udp distinction (#1605)
- Added separate TCP and UDP alternate server lists to resolve port
ambiguity.
- Enabled the use of an existing TLS counter for Alternate Server
round-robin selection, fixing a bug where only the generic counter was
used.
- Ensures correct redirection for clients using different protocols
(TCP/UDP).
- Improves reliability and prevents misdirected connections in
multi-protocol setups.

Fixes #1604

---------

Co-authored-by: CUMHUR KARAHAN <cumhur.karahan@turkcell.com.tr>
Co-authored-by: Gustavo Garcia <gustavogb@gmail.com>
Co-authored-by: Pavel Punsky <eakraly@users.noreply.github.com>
2026-07-16 18:38:55 -10:00
Paul Wayper
54fe73b9f9 Implement a minimum log level option (#1222)
The `-v` or `--verbose` options change a few things, but they have
almost no effect
on the `turnserver`. This implements a `log_min_level` setting and a
`--log-min-level`
option to allow the user to set a minimum level below which log messages
are not
emitted.

The option takes a level *name* — `debug`, `info`, `warning` or `error`
(case-insensitive) — and is available both as a command-line flag and as
a
config-file key:

```
turnserver --log-min-level=warning
```
```ini
# turnserver.conf
log-min-level=warning
```

Scale, most to least verbose: `debug` (the default, i.e. current
behaviour) →
`info` → `warning` → `error`. An unrecognised value is reported and
ignored
rather than silently reinterpreted, so no argument can leave the server
mute.

Documented in `README.turnserver`, the `turnserver(1)` man page, and
both example
configs. Covered by a new unit test, `tests/test_log_min_level.c`.

---

### Note: this also drops the `CONTROL` log level

The `TURN_LOG_LEVEL_CONTROL` level is removed from the `TURN_LOG_LEVEL`
enum.
Nothing in the tree ever logged at that level — it existed only as an
enum value
and two `switch` arms in `ns_turn_utils.c` — so no logging behaviour
changes.

Two consequences worth flagging for reviewers:

- **It is a public-header/ABI break.** `ns_turn_utils.h` is installed as
a public
header (`${includedir}/turn`), so `TURN_LOG_LEVEL` is shipped API.
Removing the
  middle enumerator renumbers `TURN_LOG_LEVEL_WARNING` (3 → 2) and
  `TURN_LOG_LEVEL_ERROR` (4 → 3). Anything already compiled against
`libturnclient` that passes `TURN_LOG_LEVEL_ERROR` still passes `4`,
which is
now above every level, so its messages would be dropped. This deserves a
  changelog note on release.

- **This is why `--log-min-level` takes a name and not a number.** With
the levels
renumbered, a numeric argument is a moving target across releases: an
earlier
revision of this PR documented `4 = errors only`, which after the enum
change
sat above every level and silenced the server entirely, fatal errors
included.
  Level names cannot drift out of sync with the enum that way.

Relatedly, `log_min_level` is typed as `TURN_LOG_LEVEL` rather than
`int`.
The enum has no negative enumerators, so it takes an unsigned underlying
type;
as an `int`, a negative minimum was promoted in `level < log_min_level`
and
filtered out *every* message instead of none.

---------

Signed-off-by: Paul Wayper <paulway@redhat.com>
Co-authored-by: Pavel Punsky <eakraly@users.noreply.github.com>
2026-07-14 10:27:46 -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
Corey Siltala
7016b565fa Update docs for drop-invalid-packets and response-origin-only-with-rfc5780 (#1918)
I attempted to use `make-man.sh` and it added a ton of man macro
formatting back in,
since it was a minor targeted fix I just updated `turnserver.1`
manually.

Fixes: https://github.com/coturn/coturn/issues/1917

---------

Co-authored-by: Corey Siltala <csiltala@atcorp.com>
Co-authored-by: Pavel Punsky <eakraly@users.noreply.github.com>
2026-05-17 19:19:08 -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
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
61332bebca Sync turnserver man page with current CLI options (#1903)
The turnserver man page had drifted from the actual CLI options the
binary accepts. The shipped `man/man1/turnserver.1` was last regenerated
on 05 June 2021, so several options added since then were missing and
one removed option was still documented.

The man page is auto-generated from `README.turnserver` via
`make-man.sh` (txt2man), so the source-of-truth edit is in the README;
the `.1` files are then regenerated.

In `README.turnserver`:
- Add 13 options that exist in `mainrelay.c` long_options[] but were
undocumented: --include-reason-string, --syslog-facility,
--drop-invalid-packets, --drop-invalid-packets-log, --udp-recvmmsg,
--respond-http-unsupported, --prometheus-address, --prometheus-path,
--version, --cpus, --no-cli, --no-rfc5780,
--response-origin-only-with-rfc5780.
- Document --sql-userdb as an alias on the existing --psql-userdb line.
- Remove the stale --ne=[1|2|3] entry (no longer parsed by the binary).

The regenerated `man/man1/turnserver.1` also picks up a backlog of
options that were already in the README but never reached the shipped
page (--software-attribute, --cli, --sock-buf-size, --raw-public-keys,
--stun-backward-compatibility, and the corrected --no-tlsv1_2 wording).

`man/man1/turnadmin.1` and `man/man1/turnutils.1` are regenerated as a
side-effect of `make-man.sh` running over all three READMEs; their
content was similarly stale relative to README.turnadmin /
README.turnutils.
2026-05-08 18:27:30 -07:00
Pavel Punsky
6e8dd731a7 Update config and Readme files about deprecated TLSv1/1.1 (#1848)
This commit only updates documentation - the change has been done in
#1693
2026-04-05 20:49:46 -07:00
Pavel Punsky
9467af5041 CLI interface is disabled by default (#1830)
cli interface is ON by default which creates a security risk (even
though requires a password) and recommended to be disabled.
Instead of just recommendation, this PR disables CLI by default and now
requires an explicit flag to enable it

If using old configuration or cli arguments to turnserver - it will log
an error message about `--no-cli` being deprecated while doing nothing
(already disabled). This log line will be removed in the future
2026-03-06 18:00:45 -08:00
Pavel Punsky
59921981bb Implement configurable buffer sizes (#1780)
# Description

Replace the hardcoded buffer sizes inside coturn to make them
configurable for different use cases (low bitrate use cases can save
memory and high bitrate use case can avoid congestion) - based on #1089

Add this feature in both sides (listener and relay connections).

# Tests

For now it is only the automated CI tests.
Confirmed with debugger that buffer sizes are set according to the
arguments.
2025-12-25 14:06:28 -08:00
Pavel Punsky
14f84fa48c [BREAKING] Deprecate response-origin-only-with-rfc5780 (#1690)
Make this true - response-origin-only will only be enabled with rfc5780 option enabled
2025-05-28 16:37:20 -07:00
Pavel Punsky
4cc076d424 [BREAKING] Invert no-stun-backward-compatibility to be default on (#1689)
Deprecate `--no-stun-backward-compatibility` and set it to true by
default
Add new option `--stun-backward-compatibility`, off by default

Update example/recommended configuration files

This is a breaking change as passing `--no-stun-backward-compatibility`
will be rejected as invalid argument
2025-05-28 16:23:33 -07:00
Pavel Punsky
368355a06f Invert RFC5780 option to default off (#1688)
Invert `--no-rfc5780` option to be true by default
Make it `--rfc5780` to enable it
Update example/recommended configuration files

Passing `--no-rfc5780` will have no effect as this is the default
behavior now
2025-05-28 15:08:57 -07:00
Pavel Punsky
94fcfadce1 [BREAKING] Reverse SOFTWARE_ATTRIBUTE_OPT to avoid inverse logic (#1598)
As part of looking at #1588 , I figured that sending `SOFTWARE`
attribute is also part of a problem as it increases messages sent out by
coturn and thus increasing amplification factor. For 4.6.2, the
additional size is 24 bytes (4 bytes attribute header, and 20 bytes for
"Coturn-4.6.2 'Gorst'")

If we are to use an example from #1588, "A 62 byte request will be met
with Coturn’s 401 Unauthorized response which is 150 bytes, a factor of
~2.42." - without SOFTWARE the response will be 126 bytes which reduces
amplification factor to ~2.

As I observed with multiple providers using coturn - some of the are
sending it. Meaning, they do not set `--no-software-attribute` - most
probably due to lack of clarity about this setting.

I believe sending SOFTWARE_ATTRIBUTE should be off by default which is
hinted in the RFC
(https://datatracker.ietf.org/doc/html/rfc8489#section-16.1.2)

Detailed changes:
- Extract setting the attribute into a function to avoid code
duplication
- This option is now not reloadable
- The option is now called `software_attribute` because inverse logic
creates multiple double-not in the code which makes it harder to read.
- `no-software_attribute` is still functional but marked as deprecated
in documentation

Test Plan:
- Run local tests with different cli arguments (new and deprecated) and
confirm SOFTWARE attribute is off by default, and added when arguments
say so
2024-12-13 09:28:45 -08:00
Nikolayshcx
eb3af26867 Add support for raw public keys (Rfc 7250) (#1458)
Resolving #1456

---------

Co-authored-by: nmegdanov <nikolay.megdanov@softavail.com>
2024-04-19 17:12:48 +02:00
czephyr
19744a4a39 added warnings for prometheus apt unavailability (#1184)
added some warnings that the prometheus implementation is unavailable
when installing through apt
coturn/coturn#1133
2023-08-20 13:17:37 -07: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
Molly Miller
902cb99849 Add configuration option for TLS 1.3 ciphersuites (#1118)
There are two different API's in OpenSSL for configuring TLS ciphers,
one for TLS 1.2 and below, and another for TLS 1.3. coturn only calls
the TLS 1.2 API when handling the `--cipher-list` configuration option,
which means that it's not possible to use non-default ciphersuites with
TLS 1.3 connections.

This PR calls appropriate OpenSSL API to allow TLS 1.3 ciphersuites to be configured.
2022-12-16 15:53:36 -08:00
Molly Miller
bc1678cc52 Regenerate manual pages from README files (#1117)
This PR fixes some typos and formatting, and regenerates the manual
pages from the README files. These changes were originally included in
#1105, however I've split them out into a separate PR as requested.
2022-12-06 17:04:13 -08:00
Pavel Punsky
22e51044cd Generate AUTHORS as Markdown, update references (#1102)
Refs #1049
2022-11-21 16:23:22 -08:00
Pavel Punsky
4bab2adba4 Use a single SSL context object (#989)
openssl allows multiple TLS version support through a single SSL_CTX
object.

This PR replaces 4 per-version SSL_CTX objects with a single object
(DTLS is not yet changed).
SSL context initialization code for openssl with modern API (>=1.1.0)
uses `TLS_server_method` and `SSL_CTX_set_min_proto_version` instead of
enabling specific TLS version. Byproduct of this is TLSv1_3 support when
used with openssl-1.1.1 and above

TLS 1.2 and TLS 1.3 cannot be disabled (as before)

Test plan:
- run_tests.sh script now runs turnserver with SSL certificate (which
enables TLS support)
- run_tests.sh now has one more basic test that uses TLS protocol

Co-authored-by: Pavel Punsky <pavel.punsky@epicgames.com>
2022-09-28 09:50:25 +02:00
Gustavo Garcia
bf54410bc7 Merge branch 'sysvinit/toggle-username-labels-upstream' of github.com:wireapp/coturn into wireapp-sysvinit/toggle-username-labels-upstream 2022-07-29 09:47:17 +02:00
Gustavo Garcia
ecdc8893ee Merge pull request #840 from lionelnicolas/feature/prometheus-port
Add ability to configure prometheus listener port
2022-07-29 01:03:58 +02:00
Molly Miller
6fd08bac3b Invert logic for handling username metrics labels. 2022-07-27 10:44:13 +02:00
Molly Miller
299fcea34c Make username labeling in metrics configurable. 2022-06-20 14:34:35 +01:00
Benjamin Porter
45e8217dbe Fix typo aa -> a 2021-11-18 10:46:31 -07:00
Lionel Nicolas
19495b2d1d Add ability to configure prometheus listener port 2021-10-22 19:47:49 -04:00
Lionel Nicolas
c42cd844de Fix typo and formatting in --prometheus documentation 2021-10-22 19:47:16 -04:00
Mészáros Mihály
708b83ea78 RESPONSE_ORIGIN attribute only if rfc5780 is on 2021-06-05 22:10:31 +02:00
Mészáros Mihály
54ef051844 Disable stun backward compatibility 2021-06-05 22:10:31 +02:00
Mészáros Mihály
eda11698f0 Add option no-rfc5780
To avoid any amplifiaction STUN binding attacks.
2021-06-05 22:10:31 +02:00
Mészáros Mihály
c55331dad2 Merge pull request #679 from rubo77/patch-1
Readme.turnserver: how to run server as a daemon
2021-03-23 09:45:34 +01:00
Mészáros Mihály
2204778ce1 Replace keep-address-family with allocation-default-address-family 2021-03-12 23:05:18 +01:00
Ruben Barkow-Kuder
5bb25dbfaf Update README.turnserver 2021-01-22 06:20:20 +01:00
Mészáros Mihály
104ab83f09 Automatically notify systemd if compiled 2021-01-10 20:57:43 +01:00
Mészáros Mihály
c6347d4c84 Fix typo 2021-01-08 09:44:03 +00:00
Ruben Barkow-Kuder
fa86379538 Readme.turnserver: how to run server as a daemon 2021-01-08 10:14:40 +01:00
Mészáros Mihály
ef7916842d Add systemd notification support 2021-01-07 17:51:34 +00:00
Mészáros Mihály
14f1630ec6 Fix stale-nonce documentation Resolves #604 2021-01-05 10:25:10 +00:00
Mészáros Mihály
27b261eb58 Disable binding logging to avoid DoS attack
* Add new option log-binding
2021-01-05 09:55:55 +00:00
Mészáros Mihály
1300021c6f Correct acme-redirect man and readme 2020-12-15 13:36:02 +00:00
Mészáros Mihály
08bb62ea88 Update README to fix #658 2020-12-08 13:40:49 +01:00