1
0
mirror of https://github.com/coturn/coturn.git synced 2026-10-24 00:08:57 +03:00
Files
coturn/tests/test_mp_peer_table.c
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

197 lines
7.3 KiB
C

/*
* SPDX-License-Identifier: BSD-3-Clause
*
* https://opensource.org/license/bsd-3-clause
*
* The multiplex-peer demux table is shared by every session on a relay thread
* and is keyed by exact peer IP:port, so one permitted peer IP covers 65535
* distinct keys. Registrations must stay within the per-session limit, and
* deregistration must not walk entries belonging to other sessions.
*/
#include "mp_peer_table.h"
#include "ns_turn_utils.h"
#include <unity.h>
#include <stdio.h>
static void *const SESSION_A = (void *)0x1000;
static void *const SESSION_B = (void *)0x2000;
static mp_peer_table table;
static ioa_addr peer(const char *ip, uint16_t port) {
ioa_addr addr = {0};
TEST_ASSERT_EQUAL_INT(0, make_ioa_addr((const uint8_t *)ip, port, &addr));
return addr;
}
void setUp(void) { mp_peer_table_init(&table, 0); }
void tearDown(void) { mp_peer_table_clean(&table); }
static void test_register_and_lookup(void) {
ioa_addr p = peer("203.0.113.7", 5000);
TEST_ASSERT_EQUAL_INT(MP_REGISTER_OK, mp_peer_table_register(&table, &p, SESSION_A));
TEST_ASSERT_EQUAL_PTR(SESSION_A, mp_peer_table_lookup(&table, &p));
TEST_ASSERT_EQUAL_size_t(1, mp_peer_table_count(&table));
/* Re-registering the same endpoint is idempotent, not a second entry. */
TEST_ASSERT_EQUAL_INT(MP_REGISTER_OK, mp_peer_table_register(&table, &p, SESSION_A));
TEST_ASSERT_EQUAL_size_t(1, mp_peer_table_count(&table));
ioa_addr other = peer("203.0.113.7", 5001);
TEST_ASSERT_NULL(mp_peer_table_lookup(&table, &other));
}
static void test_endpoint_owned_by_another_session_conflicts(void) {
ioa_addr p = peer("203.0.113.7", 5000);
TEST_ASSERT_EQUAL_INT(MP_REGISTER_OK, mp_peer_table_register(&table, &p, SESSION_A));
TEST_ASSERT_EQUAL_INT(MP_REGISTER_CONFLICT, mp_peer_table_register(&table, &p, SESSION_B));
TEST_ASSERT_EQUAL_PTR(SESSION_A, mp_peer_table_lookup(&table, &p));
TEST_ASSERT_EQUAL_size_t(1, mp_peer_table_count(&table));
TEST_ASSERT_EQUAL_size_t(0, mp_peer_table_session_count(&table, SESSION_B));
}
/* One permitted peer IP, a fresh port per SEND indication: growth must stop
* at the per-session limit. */
static void test_port_flood_is_capped_per_session(void) {
mp_peer_table_clean(&table);
mp_peer_table_init(&table, 64);
size_t accepted = 0;
size_t refused = 0;
for (unsigned int port = 1024; port < 1024 + 4096; ++port) {
ioa_addr p = peer("203.0.113.7", (uint16_t)port);
if (mp_peer_table_register(&table, &p, SESSION_A) == MP_REGISTER_OK) {
++accepted;
} else {
++refused;
}
}
TEST_ASSERT_EQUAL_size_t(64, accepted);
TEST_ASSERT_EQUAL_size_t(4096 - 64, refused);
TEST_ASSERT_EQUAL_size_t(64, mp_peer_table_count(&table));
TEST_ASSERT_EQUAL_size_t(64, mp_peer_table_session_count(&table, SESSION_A));
}
static void test_limit_is_per_session_not_global(void) {
mp_peer_table_clean(&table);
mp_peer_table_init(&table, 4);
for (unsigned int port = 1024; port < 1032; ++port) {
ioa_addr p = peer("203.0.113.7", (uint16_t)port);
mp_peer_table_register(&table, &p, SESSION_A);
}
TEST_ASSERT_EQUAL_size_t(4, mp_peer_table_session_count(&table, SESSION_A));
ioa_addr p = peer("198.51.100.4", 6000);
TEST_ASSERT_EQUAL_INT(MP_REGISTER_OK, mp_peer_table_register(&table, &p, SESSION_B));
TEST_ASSERT_EQUAL_PTR(SESSION_B, mp_peer_table_lookup(&table, &p));
}
static void test_freed_slots_are_reusable(void) {
mp_peer_table_clean(&table);
mp_peer_table_init(&table, 2);
ioa_addr p1 = peer("203.0.113.7", 5000);
ioa_addr p2 = peer("203.0.113.7", 5001);
ioa_addr p3 = peer("203.0.113.7", 5002);
TEST_ASSERT_EQUAL_INT(MP_REGISTER_OK, mp_peer_table_register(&table, &p1, SESSION_A));
TEST_ASSERT_EQUAL_INT(MP_REGISTER_OK, mp_peer_table_register(&table, &p2, SESSION_A));
TEST_ASSERT_EQUAL_INT(MP_REGISTER_LIMIT, mp_peer_table_register(&table, &p3, SESSION_A));
mp_peer_table_deregister(&table, &p1, SESSION_A);
TEST_ASSERT_NULL(mp_peer_table_lookup(&table, &p1));
TEST_ASSERT_EQUAL_INT(MP_REGISTER_OK, mp_peer_table_register(&table, &p3, SESSION_A));
TEST_ASSERT_EQUAL_size_t(2, mp_peer_table_count(&table));
}
/* Permission expiry drops every port of that peer IP, and nothing else. */
static void test_deregister_ip_drops_only_that_ip_of_that_session(void) {
ioa_addr a1 = peer("203.0.113.7", 5000);
ioa_addr a2 = peer("203.0.113.7", 5001);
ioa_addr a3 = peer("198.51.100.4", 5000);
ioa_addr b1 = peer("203.0.113.7", 6000);
mp_peer_table_register(&table, &a1, SESSION_A);
mp_peer_table_register(&table, &a2, SESSION_A);
mp_peer_table_register(&table, &a3, SESSION_A);
mp_peer_table_register(&table, &b1, SESSION_B);
mp_peer_table_deregister_ip(&table, &a1, SESSION_A);
TEST_ASSERT_NULL(mp_peer_table_lookup(&table, &a1));
TEST_ASSERT_NULL(mp_peer_table_lookup(&table, &a2));
TEST_ASSERT_EQUAL_PTR(SESSION_A, mp_peer_table_lookup(&table, &a3));
TEST_ASSERT_EQUAL_PTR(SESSION_B, mp_peer_table_lookup(&table, &b1));
TEST_ASSERT_EQUAL_size_t(2, mp_peer_table_count(&table));
}
static void test_deregister_session_honours_address_family(void) {
ioa_addr v4 = peer("203.0.113.7", 5000);
ioa_addr v6 = peer("2001:db8::1", 5000);
mp_peer_table_register(&table, &v4, SESSION_A);
mp_peer_table_register(&table, &v6, SESSION_A);
mp_peer_table_deregister_session(&table, SESSION_A, AF_INET);
TEST_ASSERT_NULL(mp_peer_table_lookup(&table, &v4));
TEST_ASSERT_EQUAL_PTR(SESSION_A, mp_peer_table_lookup(&table, &v6));
mp_peer_table_deregister_session(&table, SESSION_A, 0);
TEST_ASSERT_NULL(mp_peer_table_lookup(&table, &v6));
TEST_ASSERT_EQUAL_size_t(0, mp_peer_table_count(&table));
}
/* Deregistration must cost the session's own endpoints, not the size of the
* table every other session on the thread shares with it. */
static void test_deregistration_does_not_scan_foreign_entries(void) {
for (unsigned int port = 1024; port < 1024 + 200; ++port) {
ioa_addr noise = peer("198.51.100.4", (uint16_t)port);
TEST_ASSERT_EQUAL_INT(MP_REGISTER_OK, mp_peer_table_register(&table, &noise, SESSION_B));
}
ioa_addr own1 = peer("203.0.113.7", 5000);
ioa_addr own2 = peer("203.0.113.7", 5001);
ioa_addr own3 = peer("192.0.2.9", 7000);
mp_peer_table_register(&table, &own1, SESSION_A);
mp_peer_table_register(&table, &own2, SESSION_A);
mp_peer_table_register(&table, &own3, SESSION_A);
table.deregister_ops = 0;
mp_peer_table_deregister_ip(&table, &own1, SESSION_A);
TEST_ASSERT_EQUAL_size_t(201, mp_peer_table_count(&table));
TEST_ASSERT_TRUE(table.deregister_ops <= 6);
table.deregister_ops = 0;
mp_peer_table_deregister_session(&table, SESSION_A, 0);
TEST_ASSERT_TRUE(table.deregister_ops <= 2);
TEST_ASSERT_EQUAL_size_t(200, mp_peer_table_count(&table));
TEST_ASSERT_EQUAL_size_t(200, mp_peer_table_session_count(&table, SESSION_B));
}
int main(void) {
/* The limit warning must not spill a log file into the build environment. */
set_logfile("stdout");
UNITY_BEGIN();
RUN_TEST(test_register_and_lookup);
RUN_TEST(test_endpoint_owned_by_another_session_conflicts);
RUN_TEST(test_port_flood_is_capped_per_session);
RUN_TEST(test_limit_is_per_session_not_global);
RUN_TEST(test_freed_slots_are_reusable);
RUN_TEST(test_deregister_ip_drops_only_that_ip_of_that_session);
RUN_TEST(test_deregister_session_honours_address_family);
RUN_TEST(test_deregistration_does_not_scan_foreign_entries);
return UNITY_END();
}