diff --git a/DEPLOYMENT_TLS_AUTH.md b/DEPLOYMENT_TLS_AUTH.md deleted file mode 100644 index 845ebcd..0000000 --- a/DEPLOYMENT_TLS_AUTH.md +++ /dev/null @@ -1,72 +0,0 @@ -# TLS and Long-Term Authentication — Integration Notes - -Dieses Dokument beschreibt empfohlene Schritte, Abhängigkeiten und eine kurze Roadmap zur Integration von TLS (turns) und RFC-konformer Long-Term Authentication für den `niom-turn` MVP. - -Ziele -- TLS (turns) auf TCP/TLS Port 5349 (tokio-rustls / rustls) -- Long-Term Authentication (RFC 5389 / RFC 5766): Benutzerverwaltung + MESSAGE-INTEGRITY-Prüfung -- Sichere Speicherung von Credentials (keine Klartext-Passwörter in Produktion) - -Empfohlene Bibliotheken -- rustls + tokio-rustls — TLS für Tokio-basierte Server -- rcgen — optional für lokale self-signed certs während Tests -- ring / argon2 / scrypt — für sichere Passwort-Hashing (statt MD5/A1 in Produktion) - -Schritte (high-level) -1. TLS Listener - - Ergänze einen TCP-Listener auf Port 5349. - - Wrapping der TCP-Streams mit tokio-rustls using a configured ServerConfig (cert + private key). - - Handhabe ALPN falls nötig (z. B. `webrtc` client expectations). - -2. Socket Multiplexing und Protocols - - Behalte den UDP-Listener auf 3478 für STUN/TURN (UDP). Für TLS/TCP setze eine separate Task auf. - - Gemeinsame STUN/TURN-Nachrichtenverarbeitung (Parsing/Antworten) ist wiederverwendbar — benutze die Bibliotheks-Module (`stun.rs`, `alloc.rs`). - -3. Long-Term Auth (Credentials) - - Implementiere ein `PersistentCredentialStore`-Impl der `CredentialStore`-Trait, welches z. B. eine SQLite/Postgres-Tabelle oder Vault/Secrets-Backend nutzt. - - Verifiziere MESSAGE-INTEGRITY gemäß RFC: Client sendet `USERNAME`, `REALM`, `NONCE` und `MESSAGE-INTEGRITY`. Der Server berechnet die HMAC-SHA1 über den Nachrichtenteil entsprechend RFC und vergleicht. - - Verwende sichere Passwortspeicherung: speichere gehashte Passwörter (Argon2/Scrypt) und validiere beim Anmelden. Langfristige TURN-Konten sind oft server-seitig in Klartext/A1 gesetzt; für Produktion empfiehlt sich ein server-seitiges Secret, mit dem kurzfristige Credentials erzeugt werden (REST API zum Erstellen ephemerer Creds). - -4. Nonce / Replay-Protektion - - Implementiere Nonce-Generierung mit ausreichender Entropie und begrenzter Lebenszeit. Optional: zentrale Nonce-Store oder HMAC-gebundene Nonces. - -5. Tests und Fallbacks - - Automatisiere Tests: Unit-Tests für MESSAGE-INTEGRITY, Integrationstests mit `smoke_client` gegen UDP und TCP/TLS. - - Fallback-Mode: if TLS configs not present, start UDP-only server but clearly log the insecure state. - -Code-Snippets / Hinweise -- Beispiel (TLS-Server-Setup sketch): - -```rust -// load cert/key -let certs = load_certs("/path/to/cert.pem")?; -let key = load_private_key("/path/to/key.pem")?; -let mut cfg = rustls::ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert(certs, key)?; -let tls_acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(cfg)); - -let listener = TcpListener::bind("0.0.0.0:5349").await?; -loop { - let (socket, peer) = listener.accept().await?; - let acceptor = tls_acceptor.clone(); - tokio::spawn(async move { - match acceptor.accept(socket).await { - Ok(stream) => { - // read STUN messages from the TLS stream, process same as UDP - } - Err(e) => tracing::error!("TLS accept failed: {:?}", e), - } - }); -} -``` - -Prioritäten / Phasen -- Phase 1: Config-driven TLS listener + integration tests (low risk) -- Phase 2: Implement Persistent CredentialStore and nonce lifecycle -- Phase 3: Replace MD5/A1 usage with secure KDF-backed credential workflow (Argon2, PBKDF2) -- Phase 4: Hardening, monitoring, and deployment automation (cert renewal) - -Weiteres -- Wenn du möchtest, kann ich die TLS listener-Integration direkt als PR implementieren (schrittweise: TLS listener -> tests -> credential-store wiring). Ich kann auch Beispiel-Implementierungen für `PersistentCredentialStore` mit SQLite/SQLx anbieten. diff --git a/README.md b/README.md index d2f6513..a5e8857 100644 --- a/README.md +++ b/README.md @@ -1,210 +1,58 @@ -niom-turn -========= +# niom-turn -Minimal TURN server scaffold for the niom project (MVP). +A minimal TURN/STUN server in Rust (Tokio), including long-term authentication (REALM/NONCE + MESSAGE-INTEGRITY) and optional TLS (`turns:`). -Goals -- Provide a TURN server with long-term authentication and TLS (turns) for WebRTC clients. -- Start with a minimal, well-tested parsing/utility layer and an in-memory credential store interface that can be replaced later. +## Features -Current status -- UDP listener on 0.0.0.0:3478 (STUN/TURN) with Allocate, CreatePermission, ChannelBind, and Send flows forwarding traffic via relay sockets. -- Long-term authentication with REALM/NONCE challenges and MESSAGE-INTEGRITY validation driven by `AuthManager`. -- STUN message parser + builder in `src/stun.rs`. -- Optional TLS listener (0.0.0.0:5349) mirrors the UDP path for `turns:` clients. +- STUN Binding (basic) +- TURN Allocate / Refresh +- CreatePermission, ChannelBind, Send +- UDP relay + return path as Data Indication or ChannelData +- TCP (`turn:`) and TLS (`turns:`) control plane (stream framing) +- TURN REST credentials (optional) +- Basic limits (allocations/permissions/channel bindings + rate limits) -Design -- Modules - - `stun.rs` – STUN/TURN message parsing, MESSAGE-INTEGRITY helpers, and response builders. - - `auth.rs` – `AuthManager` orchestrates nonce minting, realm checking, and key derivation using the pluggable `CredentialStore` (default: `InMemoryStore`). - - `alloc.rs` – Relay allocation management with permission and channel tracking. - - `main.rs` / `tls.rs` – Runtime wiring for UDP and TLS listeners using the shared authentication + allocation stack. - -Authentication & credential store -- `CredentialStore` is an async trait with `get_password(username) -> Option` used by `AuthManager`. -- `AuthManager` derives the RFC long-term key (`MD5(username:realm:password)`) and validates MESSAGE-INTEGRITY while issuing signed, timestamped nonces. -- The default `InMemoryStore` is provided for tests and local dev. Swap in a production store by implementing the trait and passing it to `AuthManager`. - -How to build +## Quickstart ```bash -cd niom-turn cargo build +cargo test ``` -How to test (quick local smoke) -- Start the server in one terminal (it listens on UDP/3478): +Local start (loads `appsettings.json` from the current working directory): ```bash -cd niom-turn -cargo run -``` - -- From another machine or container, use a STUN client or `sipsak`/custom script to send a minimal STUN Binding request and observe the 401 reply. - -Next steps -- Implement full STUN attribute parsing (MESSAGE-INTEGRITY, FINGERPRINT). -- Implement long-term auth validation using MESSAGE-INTEGRITY. -- Implement Allocate + relayed sockets and permission handling. -- Add TLS listener (port 5349) using `tokio-rustls` and support `turns:`. - -Security / Deployment -- For production, run behind properly provisioned TLS certs (Let's Encrypt or mounted certs) and secure credential storage. -- Ensure UDP and TCP/TLS ports (3478/5349) are reachable from the internet when used as a public TURN server. - -Auth caveat -- The current implementation intentionally keeps things simple: credentials live in-memory, A1 keys - are derived via MD5 for RFC compatibility, and nonces are signed with HMAC-SHA1. Replace these - pieces (Argon2-backed store, modern KDFs, nonce rotation) before production rollout. See - `src/auth.rs` for the pluggable surface. - -Milestone 1 — Protocol Backlog ------------------------------- -This milestone focuses on turning the current MVP into a feature-complete TURN core that can be used -reliably by `niom-webrtc`. - -**Prioritised Backlog (live order)** -1. **TURN Data Plane Enablement** — `CreatePermission`, `ChannelBind`, Send/Data indications, and - peer forwarding so allocations actually relay packets between clients and peers. -2. **Authentication Hardening** — nonce lifecycle, realm configuration, Argon2-backed credential - storage, and detailed error handling for 401/438 responses. -3. **Allocation Lifecycle & Quotas** — timers, refresh requests, cleanup of expired allocations, - and resource limits per user/IP. -4. **Protocol Compliance Extras** — FINGERPRINT support, XOR-MAPPED-ADDRESS, IPv6 evaluation, - checksum validation, and fuzz/interop testing. -5. **Observability & Limits** — structured tracing, metrics, rate limiting, and CI coverage (incl. - the bundled `smoke_client`). - -Artifacts that track this milestone live in two places: - -1. This README section is kept up to date while the milestone is in progress. -2. Inline module docs (`//!`) inside `src/` record the current responsibilities and open backlog - items for each subsystem as we iterate. - -**Task in progress** -- TURN data plane enablement: - - [x] `CreatePermission` handling and permission tracking - - [x] `ChannelBind` setup and `Send` forwarding to peers - - [x] ChannelData framing and Data Indication responses from relay to client - -License: MIT - -Smoke-Test (End-to-End) ------------------------ -Diese Anleitung beschreibt, wie du lokal den laufenden TURN/STUN-Server prüfst und welche Ergebnisse zu erwarten sind. - -1) Server starten - -Starte den Server im Projektverzeichnis; die Ausgabe wird normal in stdout geschrieben. In meinen Tests habe ich den Server im Hintergrund gestartet und die Logs in `/tmp/niom-turn-server.log` umgeleitet: - -```bash -cd niom-turn -# Im Vordergrund (für Entwicklung) +cp appsettings.example.json appsettings.json cargo run --bin niom-turn - -# Oder im Hintergrund mit Log-Redirect -cargo run --bin niom-turn &>/tmp/niom-turn-server.log & ``` -2) Smoke-Client ausführen +## Configuration -Das Repo enthält ein kleines Test-Binary `smoke_client`, das eine STUN Binding-Request mit `USERNAME` und `MESSAGE-INTEGRITY` an `127.0.0.1:3478` sendet. +The current schema is shown in `appsettings.example.json`. +Important: `niom-turn` currently **always** loads `appsettings.json` from the **working directory**. -```bash -# Build (falls noch nicht gebaut) -cargo build --bin smoke_client +Details: `docs/config/runtime.md`. -# Ausführen -./target/debug/smoke_client -``` +## Deployment -3) Erwartetes Ergebnis +See `docs/deployment.md` for: +- systemd unit (including `WorkingDirectory=/etc/niom-turn`) +- TLS certificate/key paths +- NAT / `relay.advertised_ip` +- debugging with `journalctl` -Der Smoke-Client gibt die erhaltenen Bytes aus. Bei einer erfolgreichen MESSAGE-INTEGRITY-Prüfung sendet der Server eine STUN Success Response (Message Type 0x0101). Beispielsweise habe ich folgende Rückgabe gesehen: +## Protocol / Interop notes -``` -got 20 bytes from 127.0.0.1:3478 -[01, 01, 00, 00, 21, 12, a4, 42, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07, 07] -``` +- `CHANNEL-BIND` can implicitly create the permission for that peer (interop with common clients). +- Responses are signed using an MI mode that matches the accepted request variant. -Erklärung: -- `01 01` → STUN Success Response (0x0101) -- `21 12 a4 42` → Magic Cookie (0x2112A442) -- die folgenden 12 Bytes sind die Transaction ID (in diesem Test `07` wiederholt) +See `docs/turn_end_to_end_flow.md`. -Das bedeutet: Der Server hat die MESSAGE-INTEGRITY des Requests akzeptiert und eine 200-Antwort gesendet. +## Docs -Wenn stattdessen eine 401-Antwort (Challenge) ausgegeben wird, sieht man im Hex typischerweise einen Fehler-Response-Typ und REALM/NONCE-Attribute im Payload; das zeigt, dass die Authentifizierung nicht erfolgt ist und der Client die Challenge verarbeiten muss. +Entry point: `docs/index.md`. -Hinweis -- Die derzeitige Auth-Implementierung ist minimal und für Tests gedacht. Für Produktion bitte die README-Abschnitte "Auth caveat" beachten: sichere Credentials, TLS, und ggf. ephemeral credentials verwenden. +## License -Configuration (appsettings.json) --------------------------------- -Das Projekt kann eine JSON-Konfigdatei `appsettings.json` im Arbeitsverzeichnis lesen. Eine Beispiel-Datei `appsettings.example.json` liegt im Repository und zeigt die erwartete Struktur: - -```json -{ - "server": { - "bind": "0.0.0.0:3478", - "udp_bind": null, - "tcp_bind": null, - "tls_bind": "0.0.0.0:5349", - "enable_udp": true, - "enable_tcp": true, - "enable_tls": true, - "tls_cert": null, - "tls_key": null - }, - "relay": { - "relay_port_min": null, - "relay_port_max": null, - "relay_bind_ip": "0.0.0.0", - "advertised_ip": null - }, - "logging": { - "default_directive": "warn,niom_turn=info" - }, - "limits": { - "max_allocations_per_ip": null, - "max_permissions_per_allocation": null, - "max_channel_bindings_per_allocation": null, - - "unauth_rps": null, - "unauth_burst": null, - "binding_rps": null, - "binding_burst": null - }, - "credentials": [ - { - "username": "testuser", - "password": "secretpassword" - } - ], - "auth": { - "realm": "niom-turn.local", - "nonce_secret": null, - "nonce_ttl_seconds": 300, - "rest_secret": null, - "rest_max_ttl_seconds": 600 - } -} -``` - -Wenn `appsettings.json` vorhanden ist, befüllt der Server den Credential-Store aus `credentials` und übernimmt Realm/Nonce/REST-Einstellungen aus `auth`. - -Listener-Binds: -- `server.bind` ist der Legacy-Default (wird genutzt, wenn `udp_bind`/`tcp_bind` nicht gesetzt sind). -- TCP/TLS nutzen denselben TURN-over-Stream Handler; `turns:` läuft auf `server.tls_bind`. -- Wenn `server.enable_tls=true`, aber `tls_cert`/`tls_key` fehlen, wird der TLS-Listener übersprungen. - -Relay/NAT: -- Hinter NAT sollte `relay.advertised_ip` auf die öffentliche IP gesetzt werden, damit Clients in XOR-RELAYED-ADDRESS eine erreichbare Adresse erhalten. -- Für Firewalls ist ein fixer Relay-Port-Range (`relay_port_min/max`) sinnvoll. - -Details und Runbook: siehe `docs/config/runtime.md`. - -Deployment & TLS / Long-term Auth roadmap ------------------------------------------ -Siehe `DEPLOYMENT_TLS_AUTH.md` für eine kurze Roadmap und empfohlene Schritte zur Integration von TLS (`turns:` / TCP+TLS Port 5349) und der Implementierung von RFC-konformer Long-Term Authentication. +MIT diff --git a/docs/architecture/data_flow.md b/docs/architecture/data_flow.md index fbff544..7daaadb 100644 --- a/docs/architecture/data_flow.md +++ b/docs/architecture/data_flow.md @@ -1,22 +1,32 @@ # Packet Flow & Allocation Handling -## Komponenten -- **UDP Listener** (`udp_reader_loop`): Empfängt STUN/TURN Nachrichten auf `bind` Adresse. -- **AllocationManager**: Verwaltet Relay-Sockets je Client (`allocate_for`). -- **TLS Listener** (`tls::serve_tls`): Optional, Wrappt dieselbe Logik über TCP/TLS. +This page describes the high-level data flow through `niom-turn` (control plane and data plane). -## Ablauf (UDP) -1. `UdpSocket::recv_from` liest Paket, `parse_message` prüft STUN-Header. -2. Enthält `MESSAGE-INTEGRITY` → Username wird aus CredentialStore geladen und verifiziert. -3. Bei `ALLOCATE` → `AllocationManager` erstellt Relay-Port, sendet `XOR-RELAYED-ADDRESS` zurück. -4. Ohne gültige Credentials → Antwort `401 Unauthorized` mit `NONCE`. +## Components -## Ablauf (Relay) -- Für jedes Allocation wird ein Task gespawnt, der Pakete vom Relay-Socket liest und sie zurück zum Client sendet. -- TODO: Channel-Bindings und PeerData-Handling implementieren. +- **UDP listener**: receives STUN/TURN as well as ChannelData on UDP/3478. +- **TCP listener**: STUN/TURN over TCP/3478. +- **TLS listener**: STUN/TURN over TLS/5349 ("turns"). +- **AuthManager**: REALM/NONCE challenges, long-term auth (MI), optional TURN REST. +- **AllocationManager**: manages allocations including relay sockets, permissions, and channel bindings. -## TODOs -- Allocation Timeout & Refresh Requests. -- Permission Handling (CreatePermission). -- Bandbreitenlimits, Statistik (Prometheus). -- Shared Secret Mechanismus für Long-Term Credentials (RFC 5389/5766). +## Flow (control plane) + +1. The request is parsed as STUN/TURN (or recognized as ChannelData). +2. Auth is checked: + - without valid credentials → `401` (challenge) or `438` (stale nonce) + - with valid credentials → the request is processed; responses include `MESSAGE-INTEGRITY` + `FINGERPRINT` +3. Allocate/Refresh/Permission/ChannelBind update the allocation state. + +## Flow (relay/data plane) + +- On `ALLOCATE`, a UDP relay socket is bound and a relay loop is started. +- Packets from the peer are received by the relay and sent back to the client as **Data Indication** or **ChannelData**. +- For interop, a `CHANNEL-BIND` may implicitly create the permission for the peer. + +## Lifecycle / Housekeeping + +- Allocations are periodically checked for expiry (housekeeping) so relay tasks can end even while idle. +- The server periodically logs a compact metrics snapshot. + +See also: `docs/turn_end_to_end_flow.md` and `docs/tcp_tls_data_plane.md`. diff --git a/docs/architecture/tcp_tls_data_plane.md b/docs/architecture/tcp_tls_data_plane.md new file mode 100644 index 0000000..98f8d59 --- /dev/null +++ b/docs/architecture/tcp_tls_data_plane.md @@ -0,0 +1,98 @@ +# TCP/TLS Data Plane (TURN over TCP/TLS) in niom-turn + +This document explains **why** a TCP/TLS data plane matters for TURN and **how** `niom-turn` currently implements it. + +## Why is this needed? + +TURN has two kinds of traffic: + +- **Control plane**: STUN/TURN requests/responses (Allocate, CreatePermission, ChannelBind, Refresh, …) +- **Data plane**: application data between client and peer flowing through the relay (Send/Data Indication or ChannelData) + +In many real-world networks, **UDP is restricted or fully blocked** (corporate Wi‑Fi, mobile APNs, captive portals, proxy environments). WebRTC/ICE typically tries: + +1. UDP (fast, preferred) +2. TURN over TCP +3. TURN over TLS ("turns:") as the last, but often working option + +For TURN over TCP/TLS to be usable, not only the control plane must run over the stream, but also the **return path peer → client** (data plane) must arrive over the **same TCP/TLS connection**. + +## What does niom-turn implement? + +- Client ↔ server transport can be **UDP**, **TCP**, or **TLS**. +- The relay to the peer remains **UDP** (classic TURN UDP relay). +- For **TCP/TLS**, the server delivers the data-plane return path back to the client over the **stream**. + +This matches the common WebRTC fallback: “client↔server over TCP/TLS, peer transport over UDP”. + +## Architecture in code + +- Stream-Handler (TCP/TLS): [src/turn_stream.rs](../src/turn_stream.rs) +- TCP Listener: [src/tcp.rs](../src/tcp.rs) +- TLS Listener: [src/tls.rs](../src/tls.rs) +- Allocation + Relay: [src/alloc.rs](../src/alloc.rs) + +### Key idea: `ClientSink` + +So the relay loop can send peer packets back over different client transports, [src/alloc.rs](../src/alloc.rs) uses an abstraction: + +- `ClientSink::Udp { sock, addr }` → sends peer data via `udp.send_to(..., addr)` +- `ClientSink::Stream { tx }` → queues bytes into a writer task that writes onto the TCP/TLS stream + +When a client allocates over TCP/TLS, the allocation is created with a `ClientSink::Stream`. + +## Framing: STUN vs. ChannelData on a byte stream + +On UDP you receive datagrams; on TCP/TLS you receive a **continuous byte stream**. TURN over TCP/TLS multiplexes: + +- STUN/TURN messages (control plane) +- ChannelData frames (data plane, client → server) + +`niom-turn` therefore parses the stream in a loop as “next frame” (see `try_pop_next_frame(...)` in [src/turn_stream.rs](../src/turn_stream.rs)): + +### STUN Message + +- Header is 20 bytes +- The Length field is the body length +- Total length: $20 + length$ + +### ChannelData Frame + +- 4-byte header: `CHANNEL-NUMBER` (2) + `LENGTH` (2) +- Channel numbers are in the range `0x4000..=0x7FFF` (top bits `01`) +- Total length: $4 + length$ + +Important: for TCP/TLS, **no padding** may remain as “extra bytes” in the stream. Therefore `niom-turn` builds ChannelData as exactly `4 + len` bytes (see [src/stun.rs](../src/stun.rs)). + +### Hardening: resync & limits + +Because TCP/TLS is a byte stream, broken or malicious clients can otherwise easily “desynchronise” the parser. +`niom-turn` therefore implements in the stream parser: + +- **Magic cookie check** for STUN: invalid cookies trigger byte-wise resync (instead of waiting for huge lengths). +- **Frame size limits** (STUN body and ChannelData) to limit memory/DoS risk. +- **Max buffer limit** per connection: if the input buffer grows too large, the connection is closed. + +## Data flow (TCP/TLS) + +1. Client connects over TCP or TLS. +2. The stream handler reads frames: + - STUN/TURN requests → processed like the UDP path (auth, allocation, permission, channel bind, send, refresh) + - ChannelData (client→peer) → forwarded to the peer via the UDP relay +3. Peer sends UDP to the relay address. +4. The relay loop forwards bytes to the `ClientSink`: + - for streams: `tx.send(bytes)` → a writer task writes Data Indication or ChannelData back onto the same stream + +## Limitations / not implemented + +- No TCP relay to the peer (TURN TCP allocations / CONNECT methods like RFC6062). +- Focus is: client↔server transport over TCP/TLS + UDP relay. +- Full IPv6 operational coverage is not the focus of the MVP. + +## Tests + +- TCP Stream Data-Plane: `tests/tcp_turn.rs` +- TLS Stream Data-Plane: `tests/tls_data_plane.rs` +- Gemeinsames Framing (STUN + ChannelData): `tests/support/stream.rs` + +Wenn du willst, kann ich als nächsten Schritt die Doku um eine kurze Interop-Checkliste (WebRTC/ICE Verhalten, Candidate-Types, typische Fehlerbilder) ergänzen. diff --git a/docs/architecture/turn_end_to_end_flow.md b/docs/architecture/turn_end_to_end_flow.md new file mode 100644 index 0000000..bda84f6 --- /dev/null +++ b/docs/architecture/turn_end_to_end_flow.md @@ -0,0 +1,145 @@ +# End-to-End TURN Flow (UDP + TLS) + +This document describes the **currently implemented** end-to-end flow in `niom-turn`, based on the current MVP code: + +- UDP control plane + UDP data plane: [src/server.rs](../src/server.rs), [src/alloc.rs](../src/alloc.rs), [src/stun.rs](../src/stun.rs), [src/auth.rs](../src/auth.rs) +- TLS control plane ("turns") with STUN framing: [src/tls.rs](../src/tls.rs) +- Test builders (how requests are constructed): [tests/support/stun_builders.rs](../tests/support/stun_builders.rs) + +## Terms + +- **Client**: TURN client (e.g. a WebRTC ICE agent) +- **Server**: `niom-turn` (this project) +- **Peer**: the remote endpoint to relay to (typically another WebRTC endpoint) +- **Allocation**: server-side session providing a **UDP relay socket** +- **Permission**: permission to send to a peer / accept peer packets +- **Channel binding**: mapping `channel-number -> peer` to enable ChannelData + +--- + +## UDP: sequence diagram (happy path) + +```mermaid +sequenceDiagram + autonumber + participant C as Client (TURN) + participant S as niom-turn (UDP:3478) + participant R as Relay Socket (UDP:ephemeral) + participant P as Peer + + Note over C,S: 1) Allocate without auth → 401 challenge + C->>S: STUN Allocate Request (no MI) + S->>C: STUN Error Response 401 + REALM + NONCE + + Note over C,S: 2) Allocate with long-term auth + C->>S: STUN Allocate Request + USERNAME/REALM/NONCE + MESSAGE-INTEGRITY + S->>R: bind("0.0.0.0:0"), spawn Relay-Loop + S->>C: Allocate Success + XOR-RELAYED-ADDRESS + LIFETIME (+ MESSAGE-INTEGRITY + FINGERPRINT) + + Note over C,S: 3) Permission (CreatePermission optional) + C->>S: CreatePermission + XOR-PEER-ADDRESS (+ Auth + MI) + S->>C: Success (200) (+ MESSAGE-INTEGRITY + FINGERPRINT) + + Note over C,S: 4) Send (client→peer via relay) + C->>S: Send + XOR-PEER-ADDRESS + DATA (+ Auth + MI) + S->>R: relay.send_to(DATA, Peer) + R->>P: UDP payload (source = relay_addr) + + Note over P,C: 5) Return path (peer→client) + P->>R: UDP payload (dest = relay_addr) + alt Channel binding exists + R->>S: recv_from(Peer) + S->>C: ChannelData(channel, payload) + else No channel binding + R->>S: recv_from(Peer) + S->>C: Data Indication (METHOD_DATA|INDICATION) + XOR-PEER-ADDRESS + DATA + end + + Note over C,S: 6) Optional: ChannelBind + ChannelData + C->>S: ChannelBind + CHANNEL-NUMBER + XOR-PEER-ADDRESS (+ Auth + MI) + Note over C,S: Interop: ChannelBind may implicitly create the permission for this peer + S->>C: Success (200) (+ MESSAGE-INTEGRITY + FINGERPRINT) + C->>S: ChannelData(channel, payload) + S->>R: relay.send_to(payload, Peer) + R->>P: UDP payload + + Note over C,S: 7) Refresh + C->>S: Refresh + LIFETIME (+ Auth + MI) + S->>C: Success + LIFETIME(applied) (+ MESSAGE-INTEGRITY + FINGERPRINT) +``` + +### What the server does (concretely) + +- Entry point: `udp_reader_loop` in [src/server.rs](../src/server.rs) +- Early branch: if `parse_channel_data(...)` succeeds, the packet is **not** parsed as STUN; ChannelData is forwarded directly (only if allocation + binding + permission checks pass). +- STUN/TURN requests are parsed via `parse_message(...)` in [src/stun.rs](../src/stun.rs). + +### RFC interop note: FINGERPRINT + +- All STUN messages built by the server append `FINGERPRINT` as the last attribute. +- If a client includes `FINGERPRINT`, it is validated; invalid fingerprints cause the message to be dropped (no response). + +### Auth decisions and common error codes + +Auth policy is centralised in `AuthManager::authenticate` in [src/auth.rs](../src/auth.rs). + +- **401 Unauthorized**: when `MESSAGE-INTEGRITY` is missing → challenge with `REALM` + `NONCE`. +- **438 Stale Nonce**: when `NONCE` is expired/invalid → new challenge. +- **437 Allocation Mismatch**: when CreatePermission/Send/ChannelBind/Refresh arrives without an allocation. +- **403 Peer Not Permitted**: when a peer is not (or no longer) permitted. +- **400 Missing/Invalid ...**: when required attributes are missing or XOR-PEER-ADDRESS cannot be decoded. + +### Interop note: MESSAGE-INTEGRITY in responses + +Some clients expect responses to be signed using the same “MESSAGE-INTEGRITY variant” that was accepted for the request. +`niom-turn` therefore derives the mode from the authenticated request and uses it for all responses within the same transaction. + +--- + +## TLS (turns): sequence diagram (control plane) + +Important: the TLS implementation in [src/tls.rs](../src/tls.rs) reuses the same TURN handler as TCP and implements a real **stream data plane**. + +```mermaid +sequenceDiagram + autonumber + participant C as Client (turns) + participant T as niom-turn (TLS:5349) + participant R as Relay Socket (UDP:ephemeral) + participant P as Peer + + Note over C,T: STUN framing over TCP/TLS: read → chunk by (len+20) + C->>T: STUN Allocate (no MI) + T->>C: 401 + REALM + NONCE (over TLS) + + C->>T: STUN Allocate + Auth + MI + T->>R: allocate_for(peer, stream-sink) + T->>C: Allocate Success + XOR-RELAYED-ADDRESS + LIFETIME (over TLS) + + C->>T: CreatePermission/Send/Refresh/ChannelBind (over TLS) + T->>C: Success/Error (over TLS) + + Note over P,C: Peer data returns over the TLS stream + P->>R: UDP payload an relay_addr + R->>T: recv_from(Peer) + T->>C: Data Indication / ChannelData (over TLS) +``` + +### Consequence + +- Control plane over TLS works (Allocate/Refresh/… are answered over TLS). +- The **data-plane return path** (peer → client) also runs over the TLS stream (relay → `ClientSink::Stream`). + +More details: [docs/tcp_tls_data_plane.md](tcp_tls_data_plane.md) + +--- + +## Mini checklist: minimal flow (practical) + +1. `ALLOCATE` without MI → `401` + `REALM` + `NONCE` +2. `ALLOCATE` with `USERNAME/REALM/NONCE` + `MESSAGE-INTEGRITY` → `XOR-RELAYED-ADDRESS` + `LIFETIME` +3. Optional `CREATE_PERMISSION` for the peer → `200` +4. `SEND` with `DATA` → server sends to the peer via relay +5. Peer sends back to the relay → server delivers to the client as `DATA-INDICATION` or `CHANNEL-DATA` +6. Optional `CHANNEL_BIND` + ChannelData for a more efficient data plane (may implicitly create the permission) +7. `REFRESH` to extend, or `LIFETIME=0` to release diff --git a/docs/architecture/turn_rest_credentials.md b/docs/architecture/turn_rest_credentials.md new file mode 100644 index 0000000..59e4a9a --- /dev/null +++ b/docs/architecture/turn_rest_credentials.md @@ -0,0 +1,75 @@ +# TURN REST Credentials (Ephemeral) — Usage & Operations + +This document explains the **TURN REST credential** strategy for `niom-turn` (MVP → production-capable path). + +## Why TURN REST? + +- You want to issue *short-lived* TURN logins for WebRTC (e.g. 5–10 minutes). +- Your TURN server does not store per-user passwords. +- Your backend can mint tokens later; until then you can generate tokens locally/ops-side. + +## Scheme (compatible with common WebRTC stacks) + +**Username**: `` or `:` + +- Example: `1735412345:alice` +- `expiry_unix_seconds` is a Unix timestamp in seconds. + +**Credential (password)**: + +$$\n\text{credential} = \text{base64}(\text{HMAC-SHA1}(\text{secret}, \text{username}))\n$$ + +This `credential` is then used as the TURN password in your ICE server configuration. + +## Server side (`niom-turn`) + +Configuration in [appsettings.example.json](../appsettings.example.json): + +- `auth.rest_secret`: shared secret (must stay secret) +- `auth.rest_max_ttl_seconds`: maximum acceptable TTL window into the future. If a token is too far in the future, it is rejected (safety measure). + +Important: +- The server accepts TURN REST credentials as a fallback **only if** the username is not found in the credential store. +- Expiry/TTL rules: + - `expiry` must be >= `now` + - and `expiry - now <= rest_max_ttl_seconds` + +## Generate tokens locally (without a backend) + +This repo includes a small CLI: + +- Binary: [src/bin/turn_rest_cred.rs](../src/bin/turn_rest_cred.rs) + +Examples: + +1) Simple (stdout as env-like lines) + +`cargo run --bin turn_rest_cred -- --secret "SUPER_SECRET" --user alice --ttl 600` + +2) JSON output + +`cargo run --bin turn_rest_cred -- --secret "SUPER_SECRET" --user alice --ttl 600 --json` + +You get: +- `username` → use as `iceServers[].username` in WebRTC +- `credential` → use as `iceServers[].credential` in WebRTC + +## WebRTC example (frontend) + +In your app you typically set: + + - URLs (at least UDP + TLS): + - `turn:your-domain:3478?transport=udp` + - `turn:your-domain:3478?transport=tcp` + - `turns:your-domain:5349?transport=tcp` + +and then: +- `username`: from the generator/backend +- `credential`: from the generator/backend + +## Operational notes + +- **Secret rotation**: plan rotation (e.g. two secrets in parallel) or in-flight tokens will break. +- **Keep TTL small**: 5–10 minutes is typical. +- **Logs**: never log the `secret` or full credentials. +- **Rate limits/quotas**: add them to avoid open-relay abuse. diff --git a/docs/config/runtime.md b/docs/config/runtime.md index f2f73a9..e06c3f9 100644 --- a/docs/config/runtime.md +++ b/docs/config/runtime.md @@ -1,10 +1,10 @@ # Runtime Configuration -## Dateien -- `appsettings.example.json`: Beispiel mit Server-Bind und Test-Credentials. -- `appsettings.json`: Produktiverstellung (bind, TLS, Credentials). +## Files +- `appsettings.example.json`: example config with server binds and test credentials. +- `appsettings.json`: production config (binds, TLS, credentials). -## Struktur +## Schema ``` Config { server: ServerOptions { @@ -44,35 +44,35 @@ Config { } ``` -## Default-Fallbacks (siehe `main.rs`) -- Bind: `0.0.0.0:3478` -- Single Test Credential: `testuser` / `secretpassword` +## Default fallbacks (see `main.rs`) +- bind: `0.0.0.0:3478` +- single test credential: `testuser` / `secretpassword` -## Runbook (Start & Betrieb) +## Runbook (start & operations) -### 1) Konfiguration anlegen +### 1) Create the configuration -- Der Server lädt **immer** `appsettings.json` aus dem aktuellen Working Directory (siehe `Config::load_default()`). -- Als Basis kannst du `appsettings.example.json` nach `appsettings.json` kopieren und anpassen. +- The server currently **always** loads `appsettings.json` from the current working directory (see `Config::load_default()`). +- As a starting point, copy `appsettings.example.json` to `appsettings.json` and adjust it. -### 2) Server starten +### 2) Start the server ```bash -# im Repo-Root +# from the repo root cargo run --bin niom-turn ``` -Optional kannst du das Log-Level überschreiben: +Optionally override the log level: ```bash RUST_LOG=info cargo run --bin niom-turn ``` -Hinweise: -- Wenn `server.enable_tls=true`, aber `server.tls_cert`/`server.tls_key` fehlen, wird der TLS-Listener **übersprungen** (Info-Log). -- Beim Start loggt der Server, welche Listener aktiv sind und welche Relay-Optionen gelten. +Notes: +- If `server.enable_tls=true` but `server.tls_cert`/`server.tls_key` are missing, the TLS listener is **skipped** (info log). +- On startup, the server logs which listeners are enabled and which relay options are in effect. -### 3) Minimal-Konfig: UDP-only (einfachster Start) +### 3) Minimal config: UDP-only (simplest start) ```json { @@ -116,7 +116,7 @@ Hinweise: } ``` -### 4) Minimal-Konfig: UDP + TCP + TLS (für maximale WebRTC-Kompatibilität) +### 4) Minimal config: UDP + TCP + TLS (best WebRTC compatibility) ```json { @@ -154,19 +154,19 @@ Hinweise: } ``` -Betriebs-Checkliste (kurz): -- Firewall öffnen: UDP/3478, TCP/3478, TCP/5349 sowie den UDP-Relay-Portbereich (`relay_port_min/max`). -- Hinter NAT: `relay.advertised_ip` auf die öffentliche IP setzen. -- Für TURN REST Credentials: `auth.rest_secret` setzen und ggf. feste `credentials` leer lassen. +Operations checklist (short): +- open firewall: UDP/3478, TCP/3478, TCP/5349 and the UDP relay port range (`relay_port_min/max`). +- behind NAT: set `relay.advertised_ip` to the public IP. +- for TURN REST credentials: set `auth.rest_secret` and optionally leave static `credentials` empty. ## TODOs -- Shared Secret / REST API zur Credential-Verwaltung. -- Health-Port (HTTP) für Monitoring. -- Rate-Limits/Quota (pro Username) und Relay-Port-Range produktiv setzen. +- Shared secret / REST API for credential management. +- Health endpoint (HTTP) for monitoring. +- Rate limits/quotas (per username) and production relay port range settings. -## Hinweise für produktiven Betrieb +## Production notes -- Wenn der Server hinter NAT läuft, setze `relay.advertised_ip` auf die öffentliche IP, damit Clients in XOR-RELAYED-ADDRESS eine erreichbare Adresse erhalten. -- Für Firewalls ist ein fester Relay-Port-Range sinnvoll (`relay_port_min/max`), damit nur dieser UDP-Bereich geöffnet werden muss. +- If the server runs behind NAT, set `relay.advertised_ip` to the public IP so clients receive a reachable address in XOR-RELAYED-ADDRESS. +- A fixed relay port range (`relay_port_min/max`) is recommended so you only need to open that UDP range. -- Allocations werden auch ohne Traffic periodisch auf Expiry geprüft und bereinigt (Housekeeping), damit Relay-Sockets/Tasks nicht dauerhaft hängen bleiben. +- Allocations are periodically checked for expiry and pruned (housekeeping) even without traffic, so relay sockets/tasks do not linger indefinitely. diff --git a/docs/deploy_tls_lxc.md b/docs/deploy_tls_lxc.md deleted file mode 100644 index 6ffa0a5..0000000 --- a/docs/deploy_tls_lxc.md +++ /dev/null @@ -1,86 +0,0 @@ -# Deployment: TLS (turns) in Proxmox LXC - -This document describes a minimal, practical approach to deploy `niom-turn` as a TURN/TLS -(`turns:`) server inside a Proxmox LXC container for an initial Internet-facing MVP. - -Goals -- Run `niom-turn` with a TLS listener (TCP/TLS 5349) and UDP listener (3478). -- Ensure TLS certs are provisioned securely (Let's Encrypt or mounted files). -- Harden minimal attack surface (firewall, user, systemd supervision). - -Prerequisites -- Proxmox VE host with public IPv4 address and a reserved port mapping for the LXC. -- LXC template based on Debian/Ubuntu (minimal), with rust runtime or built binary copied in. - -Ports -- UDP 3478 — STUN/TURN (required for many clients). -- TCP 5349 — TLS/TURN (turns). - -Networking notes for LXC -- Use a bridged interface so the LXC has a routable IP (recommended). -- Alternatively, NAT the required ports from the Proxmox host to the LXC. - -Certificates -- Recommended: use Let's Encrypt with a reverse proxy or certbot on the host and mount the - certs into the container at `/etc/ssl/niom-turn/` (e.g. `fullchain.pem` and `privkey.pem`). -- Alternatively, copy PEM certs into the LXC (owner root only). For testing, you can generate - a self-signed cert with `rcgen` or `openssl`. - -Systemd service (example) - -Create `/etc/systemd/system/niom-turn.service` inside the container: - -``` -[Unit] -Description=niom-turn TURN server -After=network.target - -[Service] -User=niom -Group=niom -WorkingDirectory=/opt/niom-turn -ExecStart=/opt/niom-turn/niom-turn --config /opt/niom-turn/appsettings.json -Restart=on-failure - -[Install] -WantedBy=multi-user.target -``` - -Sample `appsettings.json` (mount next to binary) - -```json -{ - "server": { - "bind": "0.0.0.0:3478", - "tls_cert": "/etc/ssl/niom-turn/fullchain.pem", - "tls_key": "/etc/ssl/niom-turn/privkey.pem" - }, - "credentials": [ - { "username": "testuser", "password": "secretpassword" } - ] -} -``` - -Firewall -- Allow UDP 3478 and TCP 5349. If using UFW on the container host: - -```bash -ufw allow proto udp from any to any port 3478 -ufw allow proto tcp from any to any port 5349 -``` - -Security recommendations (MVP) -- Run the server as an unprivileged user (e.g. `niom`). -- Mount TLS certs read-only and restrict permissions to the service user. -- Monitor logs and open ports; add basic rate-limiting on host if available. -- For production: use ephemeral credential minting and avoid long-lived plain passwords. - -Validation steps -1. Start server via systemd: `systemctl start niom-turn` and check `journalctl -u niom-turn`. -2. From a client (or the host), run the included `smoke_client` to test the UDP path. -3. For TLS: use `openssl s_client -connect :5349 -servername ` to verify - the cert chain and that TCP connections are accepted. - -If you want, I can prepare a `systemd` unit, a small packaging script, and CI steps that build -and copy the binary into a release tarball suitable for deploying into the LXC. Let me know -which level of automation you prefer. diff --git a/docs/deployment.md b/docs/deployment.md index a516927..0ff67c7 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,136 +1,151 @@ -# Deployment Guide (niom-turn) +# Deployment (niom-turn) -This guide assumes a fresh Debian LXC (e.g., 10.0.0.22), Fritzbox port forwards are in place, and you want TURN reachable on 3478/udp+tcp and 5349/tcp with a UDP relay range (e.g., 49152-49200). +This page describes a pragmatic deployment of `niom-turn` as a systemd service. -## 1) Install dependencies +TL;DR: +- TURN/STUN: UDP + TCP on port 3478 +- TURN over TLS (`turns:`): TCP/TLS on port 5349 +- Relay ports: optional UDP port range (e.g. 49152-49200) + +## Important pitfall: config path + +The server currently **always** loads `appsettings.json` from the **current working directory** (`Config::load_default()`). + +For systemd, that means: set `WorkingDirectory=` to the directory that contains `appsettings.json`. + +## 1) Provide the binary ```bash -sudo apt update -sudo apt install -y build-essential pkg-config libssl-dev curl git systemd -# Rust toolchain (stable) -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -source "$HOME/.cargo/env" -``` - -## 2) Clone and build - -```bash -cd /opt -sudo mkdir -p niom-turn && sudo chown "$USER":"$USER" niom-turn -cd /opt/niom-turn -git clone https://github.com//niom-turn.git . cargo build --release -# Binary: target/release/niom-turn +ls -lah target/release/niom-turn ``` -## 3) Configuration +Typical layout (example): +- binary: `/opt/niom-turn/target/release/niom-turn` +- config: `/etc/niom-turn/appsettings.json` +- TLS: `/etc/niom-turn/fullchain.pem`, `/etc/niom-turn/privkey.pem` -Create config dir and place TLS cert/key (exported from NPM) and config: +## 2) Create the configuration ```bash sudo mkdir -p /etc/niom-turn -sudo chown "$USER":"$USER" /etc/niom-turn -# place /etc/niom-turn/fullchain.pem and /etc/niom-turn/privkey.pem +sudo cp appsettings.example.json /etc/niom-turn/appsettings.json +sudoedit /etc/niom-turn/appsettings.json ``` -Example `/etc/niom-turn/appsettings.json` (adjust realm, WAN IP, secrets): +Example `appsettings.json` (schema matches `src/config.rs`): ```json { - "logging": { "level": "info" }, - "auth": { - "realm": "turn.example.com", - "nonce_ttl_seconds": 600, - "rest_secret": "CHANGE_ME_REST_SECRET", - "rest_max_ttl_seconds": 86400 - }, - "listeners": { - "udp": "0.0.0.0:3478", - "tcp": "0.0.0.0:3478", - "tls": { - "addr": "0.0.0.0:5349", - "cert_file": "/etc/niom-turn/fullchain.pem", - "key_file": "/etc/niom-turn/privkey.pem" - } + "server": { + "bind": "0.0.0.0:3478", + "udp_bind": null, + "tcp_bind": null, + "tls_bind": "0.0.0.0:5349", + "enable_udp": true, + "enable_tcp": true, + "enable_tls": true, + "tls_cert": "/etc/niom-turn/fullchain.pem", + "tls_key": "/etc/niom-turn/privkey.pem" }, "relay": { - "bind_addr": "0.0.0.0", - "public_addr": "YOUR_WAN_IP", - "port_range": "49152-49200" + "relay_port_min": 49152, + "relay_port_max": 49200, + "relay_bind_ip": "0.0.0.0", + "advertised_ip": "YOUR_PUBLIC_IP_OR_HOSTNAME" }, - "rate_limits": { - "enabled": true, - "max_allocations_per_ip": 10, - "max_permissions_per_allocation": 10, - "max_channels_per_allocation": 10 + "auth": { + "realm": "turn.example.com", + "nonce_secret": null, + "nonce_ttl_seconds": 300, + "rest_secret": null, + "rest_max_ttl_seconds": 600 + }, + "credentials": [ + { "username": "testuser", "password": "secretpassword" } + ], + "logging": { + "default_directive": "warn,niom_turn=info" + }, + "limits": { + "max_allocations_per_ip": null, + "max_permissions_per_allocation": null, + "max_channel_bindings_per_allocation": null, + + "unauth_rps": null, + "unauth_burst": null, + "binding_rps": null, + "binding_burst": null } } ``` -- `public_addr` must be your public WAN IP (not the LXC IP). -- `rest_secret` is used for TURN REST credentials (time-based user/pass). +### NAT / public IP / hostnames -## 4) Systemd service +- If the server runs behind NAT, set `relay.advertised_ip` to the public IP so clients receive a reachable address in `XOR-RELAYED-ADDRESS`. +- As a workaround, `relay.relay_bind_ip` and `relay.advertised_ip` can also be hostnames; they are resolved **once at startup**. -Install binary and user: +## 3) TLS certificates + +You need PEM files: +- `fullchain.pem` (certificate chain) +- `privkey.pem` (private key) + +Set restrictive permissions, e.g.: ```bash -sudo cp /opt/niom-turn/target/release/niom-turn /usr/local/bin/niom-turn -sudo useradd --system --no-create-home --shell /usr/sbin/nologin niomturn -sudo chown root:root /usr/local/bin/niom-turn -sudo chmod 0755 /usr/local/bin/niom-turn -sudo chown -R niomturn:niomturn /etc/niom-turn +sudo chown root:root /etc/niom-turn/fullchain.pem /etc/niom-turn/privkey.pem +sudo chmod 0644 /etc/niom-turn/fullchain.pem +sudo chmod 0600 /etc/niom-turn/privkey.pem ``` -Create `/etc/systemd/system/niom-turn.service`: +## 4) systemd unit -``` +Example: `/etc/systemd/system/niom-turn.service` + +```ini [Unit] -Description=niom-turn +Description=niom-turn TURN server After=network.target [Service] User=niomturn Group=niomturn -ExecStart=/usr/local/bin/niom-turn --config /etc/niom-turn/appsettings.json -Environment=RUST_LOG=debug,niom_turn=debug + +# Important: appsettings.json is loaded from WorkingDirectory +WorkingDirectory=/etc/niom-turn +ExecStart=/opt/niom-turn/target/release/niom-turn + Restart=on-failure -RestartSec=3 -# Optional: LimitNOFILE=65535 +RestartSec=2 + +# Optional: enable temporarily for debugging +# Environment=RUST_LOG=debug,niom_turn=debug +# Environment=NIOM_TURN_DEBUG_CONFIG=1 +# Environment=NIOM_TURN_DEBUG_CONFIG_JSON=1 [Install] WantedBy=multi-user.target ``` -Enable/start: +Enable: ```bash sudo systemctl daemon-reload sudo systemctl enable --now niom-turn ``` -## 5) Firewall (LXC) +## 5) Firewall / port forwarding -Allow inbound: UDP 3478, TCP 3478, TCP 5349, UDP relay range (49152-49200). Outbound allow all. +Open/forward at least: +- UDP 3478 +- TCP 3478 +- TCP 5349 +- UDP relay range (if `relay_port_min/max` is set) -## 6) Quick checks +## 6) Debugging / checks -- Listener ports: `ss -tulpen | grep -E '3478|5349'` -- Logs: `journalctl -u niom-turn -f` -- External TCP reachability (from Hotspot): `nc -vz turn.example.com 3478` and `nc -vz turn.example.com 5349` -- STUN/TURN test: `stunclient turn.example.com 3478 -u user -p pass` (or REST creds) -- WebRTC: open webrtc-internals / about:webrtc; ensure relay candidates show your WAN IP + ports in 49152-49200. +- Ports: `ss -tulpen | grep -E '3478|5349'` +- Logs: `journalctl -u niom-turn -f -o cat` +- Nur letzte Logs: `journalctl -u niom-turn -n 200 --no-pager -o cat` -## 7) Fritzbox / Port forwards (reference) - -- UDP 3478 → 10.0.0.22:3478 -- TCP 3478 → 10.0.0.22:3478 -- TCP 5349 → 10.0.0.22:5349 -- UDP 49152-49200 → 10.0.0.22:49152-49200 -Test from external network (Hotspot), not from LAN (avoid NAT loopback assumptions). - -## 8) Tuning / next steps - -- For more logs temporarily set `RUST_LOG=trace,niom_turn=trace` in the service env. -- Consider JSON logging + metrics export if you need richer observability. -- Keep certs renewed via NPM and re-export to the LXC. diff --git a/docs/index.md b/docs/index.md index a6ffbc3..25a1002 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,28 @@ # niom-turn Docs -Dokumentationsübersicht für den TURN/STUN-Server. +Short entry point for the documentation. -- [`architecture/data_flow.md`](architecture/data_flow.md) – UDP/TLS-Loop, Allocation Manager. -- [`config/runtime.md`](config/runtime.md) – Appsettings & Credentials. -- [`turn_end_to_end_flow.md`](turn_end_to_end_flow.md) – Sequenzgrafik: Allocate → Permission → Send/ChannelData → Rückweg (UDP + TLS). -- [`tcp_tls_data_plane.md`](tcp_tls_data_plane.md) – Warum TCP/TLS Data-Plane wichtig ist und wie sie implementiert ist. -- [`mvp_gaps_and_rfc_notes.md`](mvp_gaps_and_rfc_notes.md) – MVP-Lücken, RFC-Notizen und Auswirkungen. -- [`turn_rest_credentials.md`](turn_rest_credentials.md) – TURN REST Credentials: Algorithmus, CLI-Tool und Betrieb. -- [`testing.md`](testing.md) – Testübersicht: was abgedeckt ist und wie man es ausführt. -- [`testing_todo.md`](testing_todo.md) – Vorschläge für zusätzliche Tests (Roadmap). -- Bereits vorhanden: `deploy_tls_lxc.md`, RFC-Referenzen (STUN/TURN Specs). +## Start here -## Zielsetzung -- Production-ready TURN mit Authentifizierung, Lebenszeitverwaltung und Monitoring. -- Optionales TLS (TURN over TLS) für restriktive Netzwerke. +- [`deployment.md`](deployment.md) – systemd deployment, TLS, NAT/`advertised_ip`, debugging. +- [`config/runtime.md`](config/runtime.md) – `appsettings.json` schema and defaults. + +## Protocol flows + +- [`architecture/turn_end_to_end_flow.md`](architecture/turn_end_to_end_flow.md) – Happy path: Allocate → (Permission) → Send/ChannelData → return path. +- [`architecture/turn_rest_credentials.md`](architecture/turn_rest_credentials.md) – TURN REST Credentials + CLI. + +## Architecture + +- [`architecture/data_flow.md`](architecture/data_flow.md) – components and data flow. +- [`tcp_tls_data_plane.md`](architecture/tcp_tls_data_plane.md) – stream framing and TCP/TLS data plane. + +## Testing + +- [`testing.md`](testing.md) – test overview and commands. + +## Notes & specs + +- [`mvp_gaps_and_rfc_notes.md`](mvp_gaps_and_rfc_notes.md) – MVP gaps and RFC notes. +- [`specs/rfc5389-stun.txt`](specs/rfc5389-stun.txt) – STUN RFC (text copy). +- [`specs/rfc5766-turn.txt`](specs/rfc5766-turn.txt) – TURN RFC (text copy). diff --git a/docs/mvp_gaps_and_rfc_notes.md b/docs/mvp_gaps_and_rfc_notes.md index 1658015..e600196 100644 --- a/docs/mvp_gaps_and_rfc_notes.md +++ b/docs/mvp_gaps_and_rfc_notes.md @@ -1,114 +1,114 @@ -# MVP-Lücken & RFC-Notizen (STUN/TURN) +# MVP Gaps & RFC Notes (STUN/TURN) -Dieses Dokument listet **bewusst vereinfachte/fehlende Teile** im aktuellen `niom-turn` MVP auf, jeweils mit kurzer Auswirkung und Code-Anker. +This document lists **intentionally simplified / missing pieces** in the current `niom-turn` MVP, with a short impact note and a code anchor for each. -> Ziel: Klarheit, was schon interoperabel ist, und wo (für Production/Interop) noch Arbeit nötig ist. +> Goal: clarity on what is already interoperable, and what still needs work for production / broader interop. --- ## 1) STUN Binding ist minimal -**Ist-Zustand** -- `METHOD_BINDING` beantwortet der Server als "Success" und enthält `XOR-MAPPED-ADDRESS` (IPv4+IPv6): [src/stun.rs](../src/stun.rs) +**Current state** +- The server answers `METHOD_BINDING` with "Success" and includes `XOR-MAPPED-ADDRESS` (IPv4 + IPv6): [src/stun.rs](../src/stun.rs) -**Auswirkung** -- Für STUN-Diagnose/ICE-NAT-Discovery ist das damit deutlich interoperabler. +**Impact** +- This improves interop for STUN diagnostics / ICE NAT discovery. --- -## 2) IPv6 wird nicht unterstützt (historisch) +## 2) IPv6 support (historical note) -**Ist-Zustand** - - XOR-Address Encoding/Decoding unterstützt IPv4 **und** IPv6 (XOR-Key: Magic Cookie + Transaction ID): [src/stun.rs](../src/stun.rs) +**Current state** + - XOR-Address encoding/decoding supports IPv4 **and** IPv6 (XOR key: magic cookie + transaction ID): [src/stun.rs](../src/stun.rs) -**Auswirkung** -- TURN/STUN kann IPv6-Adressen in XOR-ADDRESS Attributen korrekt verarbeiten. +**Impact** +- TURN/STUN can correctly handle IPv6 addresses in XOR-ADDRESS attributes. --- -## 3) TURN Allocate ist stark vereinfacht +## 3) TURN Allocate is heavily simplified -**Ist-Zustand** -- `allocate_for` bindet immer ein UDP-Relay auf `0.0.0.0:0`: [src/alloc.rs](../src/alloc.rs) -- `REQUESTED-TRANSPORT` wird nicht ausgewertet (es gibt nur UDP-Relay). -- Weitere RFC-TURN Optionen (EVEN-PORT, RESERVATION-TOKEN, DONT-FRAGMENT, etc.) sind nicht implementiert. +**Current state** +- `allocate_for` always binds a UDP relay on `0.0.0.0:0`: [src/alloc.rs](../src/alloc.rs) +- `REQUESTED-TRANSPORT` is not evaluated (there is only UDP relay). +- Additional RFC TURN options (EVEN-PORT, RESERVATION-TOKEN, DONT-FRAGMENT, etc.) are not implemented. -**Auswirkung** -- Das MVP deckt den typischen UDP-Relay-Fall ab, aber nicht die volle TURN-Feature-Matrix. +**Impact** +- The MVP covers the common UDP relay case, but not the full TURN feature matrix. --- -## 4) Data Plane: UDP-relay ja, TCP-relay nein +## 4) Data plane: UDP relay yes, TCP relay no -**Ist-Zustand** -- Relay-Datenpfad ist UDP-only (Relay-Socket ist `tokio::net::UdpSocket`): [src/alloc.rs](../src/alloc.rs) -- TLS-Listener re-used Control-Plane Logik, aber kein TCP-Relay: [src/tls.rs](../src/tls.rs) +**Current state** +- The relay data path is UDP-only (the relay socket is `tokio::net::UdpSocket`): [src/alloc.rs](../src/alloc.rs) +- The TLS listener reuses the control-plane logic; there is no separate TCP relay: [src/tls.rs](../src/tls.rs) -**Auswirkung** -- `turns:` (TLS) kann Control-Plane, aber der Rückweg der relayed Daten läuft derzeit weiterhin über UDP an die Client-UDP-Adresse. -- Für echte TURN-over-TCP/TLS Data Plane (Interoperabilität mit restriktiven Netzwerken) fehlt ein eigener TCP-Relay-Pfad. +**Impact** +- `turns:` (TLS) supports the control plane, and for stream clients the **return path** (server → client) is delivered over the stream as Data Indications / ChannelData. +- However, the actual relay socket towards the peer is still UDP-only. A true TURN-over-TCP/TLS data plane with a dedicated TCP relay path is not implemented. --- -## 5) Allocation- und Timer-Verhalten ist MVP-artig +## 5) Allocation and timer behaviour is MVP-style -**Ist-Zustand** -- Allocation-Lifetime wird über `refresh_allocation` gesetzt/geklemmt: [src/alloc.rs](../src/alloc.rs) -- `remove_allocation` entfernt den Eintrag. -- Der Relay-Task (spawn in `allocate_for`) läuft jedoch weiter und prüft nur noch, ob Allocation existiert: [src/alloc.rs](../src/alloc.rs) +**Current state** +- Allocation lifetime is set/clamped via `refresh_allocation`: [src/alloc.rs](../src/alloc.rs) +- `remove_allocation` removes the entry. +- The relay task (spawned in `allocate_for`) can continue running and primarily checks whether the allocation still exists: [src/alloc.rs](../src/alloc.rs) -**Auswirkung** -- Bei vielen Allocations kann das zu unnötigen Hintergrund-Tasks führen (Resource-Management/Backpressure fehlt). +**Impact** +- With many allocations, this can lead to unnecessary background tasks (resource management / backpressure is minimal). --- -## 6) Permissions & ChannelBindings sind minimal +## 6) Permissions & channel bindings are minimal -**Ist-Zustand** -- Permission TTL ist statisch (300s) und wird nur durch erneutes `CreatePermission` erneuert: [src/alloc.rs](../src/alloc.rs) -- ChannelBindings nutzen denselben TTL-Wert (`PERMISSION_LIFETIME`): [src/alloc.rs](../src/alloc.rs) +**Current state** +- Permission TTL is static (300s) and is only renewed by sending `CreatePermission` again: [src/alloc.rs](../src/alloc.rs) +- Channel bindings use the same TTL value (`PERMISSION_LIFETIME`): [src/alloc.rs](../src/alloc.rs) -**Auswirkung** -- Funktional ok für MVP, aber nicht unbedingt exakt RFC-getreu im Detail (separate Lifetime-Regeln/Refresh-Strategien sind üblich). +**Impact** +- Functionally OK for an MVP, but not necessarily RFC-exact in all details (separate lifetime rules / refresh strategies are common). --- ## 7) MESSAGE-INTEGRITY/FINGERPRINT -**Ist-Zustand** -- `MESSAGE-INTEGRITY` wird RFC-konform geprüft (HMAC-SHA1 über die Message bis inkl. MI-Attribut; Header-Länge wird dafür auf „Ende von MI“ gesetzt, d.h. kompatibel mit nachfolgenden Attributen wie `FINGERPRINT`): [src/stun.rs](../src/stun.rs) -- Für TURN-Responses nach erfolgreicher Authentisierung hängt der Server `MESSAGE-INTEGRITY` an und setzt `FINGERPRINT` als letztes Attribut: [src/server.rs](../src/server.rs), [src/turn_stream.rs](../src/turn_stream.rs) -- `FINGERPRINT` wird bei allen vom Server gebauten STUN-Nachrichten als letztes Attribut angehängt und bei eingehenden Nachrichten (falls vorhanden) validiert: [src/stun.rs](../src/stun.rs) +**Current state** +- `MESSAGE-INTEGRITY` is validated in an RFC-compatible way (HMAC-SHA1 over the message up to and including the MI attribute; the header length is set to the “end of MI”, which is compatible with subsequent attributes like `FINGERPRINT`): [src/stun.rs](../src/stun.rs) +- For TURN responses after successful authentication, the server appends `MESSAGE-INTEGRITY` and sets `FINGERPRINT` as the last attribute: [src/server.rs](../src/server.rs), [src/turn_stream.rs](../src/turn_stream.rs) +- `FINGERPRINT` is appended as the last attribute on all STUN messages built by the server, and validated on inbound messages (if present): [src/stun.rs](../src/stun.rs) -**Auswirkung** -- Bessere Browser/ICE-Interop und leichtes Hardening (Messages mit ungültigem `FINGERPRINT` werden verworfen). +**Impact** +- Better browser/ICE interop and mild hardening (messages with invalid `FINGERPRINT` are dropped). --- -## 8) Observability / Limits / Hardening fehlen (noch) +## 8) Observability / limits / hardening (still missing) -**Ist-Zustand** -- Keine Quotas pro User/IP, keine Rate-Limits, keine Bandbreitenlimits pro Allocation. -- Credential Store ist In-Memory (Test/Dev): [src/auth.rs](../src/auth.rs), Trait: [src/traits/credential_store.rs](../src/traits/credential_store.rs) +**Current state** +- No quotas per user/IP, no rate limits, no bandwidth limits per allocation. +- Credential store is in-memory (test/dev): [src/auth.rs](../src/auth.rs), trait: [src/traits/credential_store.rs](../src/traits/credential_store.rs) -**Auswirkung** -- Für Production braucht es Limits, persistente Credentials, Monitoring/Metrics und härteres Error-Handling. +**Impact** +- For production you likely want limits, persistent credentials, monitoring/metrics, and stricter error handling. --- -## 9) Weitere RFC-Ecken (nicht implementiert) +## 9) More RFC corners (not implemented) -Typische Punkte, die im MVP fehlen/noch offen sind: -- Full attribute coverage (z.B. UNKNOWN-ATTRIBUTES, SOFTWARE, etc.) -- Vollständige STUN/TURN Class/Method Encoding nach RFC (hier bewusst vereinfacht über `METHOD | CLASS_*`): [src/constants.rs](../src/constants.rs) -- IPv6, Hairpinning-Sonderfälle, NAT-bezogene Interop-Edge-Cases +Typical items that are missing / still open in the MVP: +- Full attribute coverage (e.g. UNKNOWN-ATTRIBUTES, SOFTWARE, etc.) +- Fully RFC-accurate STUN/TURN class/method encoding (here intentionally simplified via `METHOD | CLASS_*`): [src/constants.rs](../src/constants.rs) +- IPv6 hairpinning edge cases, NAT-related interop edge cases --- -## Was bereits gut abgedeckt ist +## What is already covered well -- End-to-end UDP TURN Core: `Allocate` → `CreatePermission` → `Send`/ChannelData → Rückweg als Data Indication/ChannelData. -- Long-term Auth (Realm/Nonce + MI) mit klaren 401/438/437/403 Pfaden. -- TLS Listener (Control-Plane) mit STUN-Framing über TCP/TLS. +- End-to-end UDP TURN core: `Allocate` → `CreatePermission` → `Send`/ChannelData → return path as Data Indication/ChannelData. +- Long-term auth (realm/nonce + MI) with clear 401/438/437/403 paths. +- TLS listener (control plane) with STUN framing over TCP/TLS. -Siehe auch die Integrationstests: [tests/udp_turn.rs](../tests/udp_turn.rs) und [tests/tls_turn.rs](../tests/tls_turn.rs). +See also the integration tests: [tests/udp_turn.rs](../tests/udp_turn.rs) and [tests/tls_turn.rs](../tests/tls_turn.rs). diff --git a/docs/plan-by-gpt5.odt b/docs/plan-by-gpt5.odt deleted file mode 100644 index b7660cd..0000000 Binary files a/docs/plan-by-gpt5.odt and /dev/null differ diff --git a/docs/rfc5389-stun.txt b/docs/specs/rfc5389-stun.txt similarity index 100% rename from docs/rfc5389-stun.txt rename to docs/specs/rfc5389-stun.txt diff --git a/docs/rfc5766-turn.txt b/docs/specs/rfc5766-turn.txt similarity index 100% rename from docs/rfc5766-turn.txt rename to docs/specs/rfc5766-turn.txt diff --git a/docs/tcp_tls_data_plane.md b/docs/tcp_tls_data_plane.md deleted file mode 100644 index 5ea71b7..0000000 --- a/docs/tcp_tls_data_plane.md +++ /dev/null @@ -1,98 +0,0 @@ -# TCP/TLS Data-Plane (TURN over TCP/TLS) in niom-turn - -Dieses Dokument erklärt **wofür** eine TCP/TLS Data-Plane bei TURN gebraucht wird und **wie** `niom-turn` sie (aktuell) implementiert. - -## Wofür wird das benötigt? - -TURN hat zwei Arten von Traffic: - -- **Control-Plane**: STUN/TURN Requests/Responses (Allocate, CreatePermission, ChannelBind, Refresh, …) -- **Data-Plane**: Nutzdaten zwischen Client und Peer, die über das Relay laufen (Send/Data-Indication oder ChannelData) - -In vielen realen Netzen ist **UDP eingeschränkt oder komplett blockiert** (Corporate WLAN, Mobilfunk-APNs, Captive Portals, Proxy-Umgebungen). WebRTC/ICE versucht deshalb typischerweise: - -1. UDP (schnell, bevorzugt) -2. TURN über TCP -3. TURN über TLS ("turns:") als letzte, aber oft funktionierende Option - -Damit TURN über TCP/TLS wirklich nutzbar ist, muss nicht nur die Control-Plane über den Stream laufen, sondern auch der **Rückweg Peer → Client** (Data-Plane) über **dieselbe TCP/TLS Verbindung** beim Client ankommen. - -## Was implementiert niom-turn konkret? - -- Client ↔ Server Transport kann **UDP**, **TCP** oder **TLS** sein. -- Das Relay zum Peer ist weiterhin **UDP** (klassisches TURN UDP-Relay). -- Bei **TCP/TLS** liefert der Server die Data-Plane zurück an den Client über den **Stream** (statt über UDP an die Client-Adresse zu senden). - -Das entspricht dem üblichen WebRTC-Fallback: "Client-Server über TCP/TLS, Peer-Transport über UDP". - -## Architektur im Code - -- Stream-Handler (TCP/TLS): [src/turn_stream.rs](../src/turn_stream.rs) -- TCP Listener: [src/tcp.rs](../src/tcp.rs) -- TLS Listener: [src/tls.rs](../src/tls.rs) -- Allocation + Relay: [src/alloc.rs](../src/alloc.rs) - -### Schlüsselidee: `ClientSink` - -Damit der Relay-Loop Peer-Pakete an unterschiedliche Client-Transporte schicken kann, gibt es in [src/alloc.rs](../src/alloc.rs) eine Abstraktion: - -- `ClientSink::Udp { sock, addr }` → sendet Peer-Daten per `udp.send_to(..., addr)` -- `ClientSink::Stream { tx }` → queued Bytes in einen Writer-Task, der auf den TCP/TLS Stream schreibt - -Wenn ein Client über TCP/TLS allocatet, wird die Allocation mit einem `ClientSink::Stream` erzeugt. - -## Framing: STUN vs. ChannelData auf einem Byte-Stream - -Auf UDP bekommt man Datagramme; auf TCP/TLS bekommt man einen **kontinuierlichen Byte-Stream**. TURN over TCP/TLS multiplexed: - -- STUN/TURN Messages (Control-Plane) -- ChannelData Frames (Data-Plane, Client → Server) - -`niom-turn` parst daher im Stream in einer Schleife "nächstes Frame" (siehe `try_pop_next_frame(...)` in [src/turn_stream.rs](../src/turn_stream.rs)): - -### STUN Message - -- Header ist 20 Bytes -- Length-Feld ist die Body-Länge -- Gesamtlänge ist: $20 + length$ - -### ChannelData Frame - -- 4 Byte Header: `CHANNEL-NUMBER` (2) + `LENGTH` (2) -- Channel-Nummern liegen im Bereich `0x4000..=0x7FFF` (Top-Bits `01`) -- Gesamtlänge ist: $4 + length$ - -Wichtig: Bei TCP/TLS darf **kein Padding** als "separate Bytes" im Stream verbleiben. Deshalb baut `niom-turn` ChannelData als exakt `4 + len` Bytes (siehe [src/stun.rs](../src/stun.rs)). - -### Hardening: Resync & Limits - -Da TCP/TLS ein Byte-Stream ist, können kaputte oder bösartige Clients den Parser sonst leicht „desynchronisieren“. -`niom-turn` implementiert daher im Stream-Parser: - -- **Magic-Cookie Check** für STUN: Ungültige Cookies führen zu einem Byte-weisen Resync (statt auf riesige Längen zu warten). -- **Frame-Size Limits** (STUN-Body und ChannelData), um Speicher-/DoS-Risiken zu begrenzen. -- **Max Buffer Limit** pro Verbindung: wenn der Eingangspuffer zu groß wird, wird die Verbindung geschlossen. - -## Datenfluss (TCP/TLS) - -1. Client verbindet sich per TCP oder TLS. -2. Der Stream-Handler liest Frames: - - STUN/TURN Requests → verarbeitet wie UDP-Pfad (Auth, Allocation, Permission, ChannelBind, Send, Refresh) - - ChannelData (Client→Peer) → wird über das UDP-Relay an den Peer geschickt -3. Peer sendet UDP an die Relay-Adresse. -4. Relay-Loop leitet die Bytes an den `ClientSink` weiter: - - bei Stream: `tx.send(bytes)` → Writer-Task schreibt Data-Indication oder ChannelData zurück auf denselben Stream - -## Grenzen / Noch nicht implementiert - -- Kein TCP-Relay zum Peer (TURN TCP allocations / CONNECT-Methoden wie in RFC6062). -- Fokus liegt auf: Client-Server Transport über TCP/TLS + UDP-Relay. -- IPv6 ist im aktuellen MVP noch nicht vollständig umgesetzt. - -## Tests - -- TCP Stream Data-Plane: `tests/tcp_turn.rs` -- TLS Stream Data-Plane: `tests/tls_data_plane.rs` -- Gemeinsames Framing (STUN + ChannelData): `tests/support/stream.rs` - -Wenn du willst, kann ich als nächsten Schritt die Doku um eine kurze Interop-Checkliste (WebRTC/ICE Verhalten, Candidate-Types, typische Fehlerbilder) ergänzen. diff --git a/docs/testing.md b/docs/testing.md index 4d7445b..e700aea 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,60 +1,60 @@ # Testing -Dieses Projekt ist so aufgebaut, dass sich die wichtigsten TURN-Pfade als **Unit- und Integrationstests** ausführen lassen. +This project is structured so the most important TURN paths can be exercised via **unit and integration tests**. -## Schnellstart +## Quick start -- Alle Tests: `cargo test` -- Mit weniger Output: `cargo test -q` -- Mit Logs (Beispiele): +- All tests: `cargo test` +- Less output: `cargo test -q` +- With logs (examples): - `RUST_LOG=warn,niom_turn=info cargo test -- --nocapture` -Hinweis: Die Integrationstests initialisieren `tracing` über die Helpers in `tests/support`. +Note: integration tests initialise `tracing` via helpers in `tests/support`. -## Was wird getestet? +## What is covered? ### STUN RFC-Interop (FINGERPRINT) -- Unit-Tests prüfen, dass der Server `FINGERPRINT` an Responses anhängt und dass die CRC32/XOR Validierung fehlschlägt, wenn die Nachricht manipuliert wird. +- Unit tests verify that the server appends `FINGERPRINT` to responses and that CRC32/XOR validation fails when the message is tampered with. -Siehe: Unit-Tests in [src/stun.rs](../src/stun.rs). +See: unit tests in [src/stun.rs](../src/stun.rs). ### STUN RFC-Interop (MESSAGE-INTEGRITY) -- Unit-Tests prüfen `MESSAGE-INTEGRITY` Validierung (inkl. Fall „MI + nachfolgendes FINGERPRINT“). -- UDP-Integrationstests prüfen, dass Responses nach erfolgreicher Authentisierung `MESSAGE-INTEGRITY` enthalten und validierbar sind. +- Unit tests verify `MESSAGE-INTEGRITY` validation (including the “MI + trailing FINGERPRINT” case). +- UDP integration tests verify that responses after successful authentication contain `MESSAGE-INTEGRITY` and are verifiable. ### UDP (turn:) -- Auth-Challenge (401 + NONCE) und erfolgreicher Allocate -- Refresh mit `LIFETIME=0` entfernt Allocation -- CreatePermission + Send → Peer erhält UDP Payload über Relay +- Auth challenge (401 + NONCE) and successful allocate +- Refresh with `LIFETIME=0` releases the allocation +- CreatePermission + Send → peer receives UDP payload via the relay -Siehe: `tests/udp_turn.rs` sowie die thematischen Ordner in `tests/`. +See: `tests/udp_turn.rs` and the topic folders under `tests/`. -### TLS (turns:) / Stream-basierte Data-Plane +### TLS (turns:) / stream-based data plane -- Allocate/Refresh über TLS-Stream -- (Neu) Data-Plane Rückweg Peer→Client über denselben TLS-Stream (Data-Indication oder ChannelData) +- Allocate/Refresh over the TLS stream +- Data-plane return path peer→client over the same TLS stream (Data Indication or ChannelData) -Siehe: `tests/tls_turn.rs` und `tests/tls_data_plane.rs`. +See: `tests/tls_turn.rs` and `tests/tls_data_plane.rs`. -### TCP (turn:?transport=tcp) / Stream-basierte Data-Plane +### TCP (turn:?transport=tcp) / stream-based data plane -- (Neu) Allocate/CreatePermission/Send über TCP -- (Neu) Peer→Client Rückweg als STUN Data Indication über TCP -- (Neu) ChannelBind + ChannelData in beide Richtungen +- Allocate/CreatePermission/Send over TCP +- Peer→client return path as STUN Data Indication over TCP +- ChannelBind + ChannelData in both directions -Siehe: `tests/tcp_turn.rs`. +See: `tests/tcp_turn.rs`. -## Test-Hilfen +## Test helpers -- STUN/TURN Builder: `tests/support/stun_builders.rs` -- Stream-Framing (STUN + ChannelData über TCP/TLS): `tests/support/stream.rs` -- TLS Test-Certs: `tests/support/tls.rs` +- STUN/TURN builder: `tests/support/stun_builders.rs` +- Stream framing (STUN + ChannelData over TCP/TLS): `tests/support/stream.rs` +- TLS test certs: `tests/support/tls.rs` -## Erweiterungsideen (nächste sinnvolle Abdeckung) +## Expansion ideas (useful next coverage) -- Split-Reads/Writes (Frames in mehreren TCP Reads) als Regressionstest -- IPv6 Encode/Decode Tests für XOR-ADDRESS Varianten -- Negative Tests: Peer nicht permitted, Channel ohne Bind, Allocation Timeout +- Split reads/writes (frames spanning multiple TCP reads) as regression tests +- IPv6 encode/decode tests for XOR-ADDRESS variants +- Negative tests: peer not permitted, channel without binding, allocation timeout diff --git a/docs/testing_todo.md b/docs/testing_todo.md index 7791b7e..07956c3 100644 --- a/docs/testing_todo.md +++ b/docs/testing_todo.md @@ -1,59 +1,57 @@ -# Test-ToDo (Vorschläge) +# Testing TODO (ideas) -Dieses Dokument sammelt **konkrete** Test-Ideen, die den sicheren/stabilen Betrieb (insb. unter Last/Fehlverhalten) absichern sollen. +This document collects **concrete** test ideas to increase safety/stability (especially under load and misbehaviour). -## Stream (TCP/TLS) Robustheit +## Stream (TCP/TLS) robustness -- Split-Reads: STUN Header (20B) in 2 Reads, Body in mehreren Reads -- Split-Reads: ChannelData Header (4B) und Payload getrennt -- Mixed Frames: STUN → ChannelData → STUN in einem Read (und in mehreren Reads) -- Oversize Frames: - - STUN Length > Max → Verbindung wird geschlossen (oder Frame gedroppt, je nach Policy) - - ChannelData Length > Max → Verbindung wird geschlossen (oder Frame gedroppt) -- Garbage Resync: - - Zufallsbytes vor gültigem STUN (bereits abgedeckt) - - Zufallsbytes zwischen gültigen Frames +- Split reads: STUN header (20B) in 2 reads, body in multiple reads +- Split reads: ChannelData header (4B) and payload separated +- Mixed frames: STUN → ChannelData → STUN in a single read (and across multiple reads) +- Oversize frames: + - STUN length > max → close connection (or drop frame, depending on policy) + - ChannelData length > max → close connection (or drop frame) +- Garbage resync: + - random bytes before valid STUN (already covered) + - random bytes between valid frames -## TURN Flows (Happy + Negative) +## TURN flows (happy + negative) -- Negative pro Methode (UDP/TCP/TLS jeweils): - - ohne Allocation → 437 Allocation Mismatch - - ohne Permission → 403 Peer Not Permitted - - ChannelData ohne ChannelBind → drop + optional log counter - - Stale Nonce → 438 - - falsches MI → 401/403 je nach Policy +- Negative cases per method (for UDP/TCP/TLS each): + - without allocation → 437 Allocation Mismatch + - without permission → 403 Peer Not Permitted + - ChannelData without ChannelBind → drop + optional log counter + - stale nonce → 438 + - wrong MI → 401/403 depending on policy ## Auth - TURN REST: - - abgelaufener Username → reject - - Username zu weit in der Zukunft (max TTL) → reject - - falsches HMAC/base64 → reject - - „user exists in store“ vs. „REST fallback“ Priorität + - expired username → reject + - username too far in the future (max TTL) → reject + - wrong HMAC/base64 → reject + - priority: “user exists in store” vs. “REST fallback” ## Lifecycle - Allocation expiry: - - Refresh verkürzt/verlängert, Min/Max Lifetime - - Expiry entfernt Allocation und beendet Relay-Task (keine Task-Leaks) + - Refresh shortens/extends; min/max lifetime + - Expiry removes allocation and ends relay task (no task leaks) - Permission expiry: - - Peer wird nach Ablauf verworfen + - peer is dropped after expiry - Channel binding expiry: - - Rückweg fällt auf Data Indication zurück, wenn Binding abläuft + - return path falls back to Data Indication when the binding expires -## Abuse-/DoS Prevention (sobald Limits implementiert sind) +## Abuse/DoS prevention (once limits are implemented) -- Rate limit: auth failures pro IP/Username -- Max allocations pro IP -- Max permissions/channels pro allocation -- Bandwidth caps (bytes/s) pro allocation -- Backpressure: Writer-Queue voll → Verhalten definieren (drop/close) +- Rate limit: auth failures per IP/username +- Max allocations per IP +- Max permissions/channels per allocation +- Bandwidth caps (bytes/s) per allocation +- Backpressure: writer queue full → define behaviour (drop/close) -## Interop (manuell reproduzierbar, aber dokumentiert) +## Interop (manually reproducible, but documented) -- Browser Plan: - - Trickle ICE / webrtc-internals: forced relay - - UDP-only block: erwarte TCP/TLS fallback - - `turns:` mit self-signed vs. valid cert - -Wenn du willst, kann ich als nächsten Schritt aus diesen Punkten eine priorisierte Test-Roadmap machen (P0/P1/P2) und direkt die nächsten P0-Tests implementieren. +- Browser plan: + - trickle ICE / webrtc-internals: forced relay + - UDP-only block: expect TCP/TLS fallback + - `turns:` with self-signed vs. valid cert diff --git a/docs/turn_end_to_end_flow.md b/docs/turn_end_to_end_flow.md deleted file mode 100644 index a196952..0000000 --- a/docs/turn_end_to_end_flow.md +++ /dev/null @@ -1,139 +0,0 @@ -# End-to-End TURN Flow (UDP + TLS) - -Dieses Dokument beschreibt den **konkret implementierten** End-to-End Ablauf in `niom-turn` anhand des aktuellen Codes (MVP): - -- UDP-Control-Plane und UDP-Data-Plane: [src/server.rs](../src/server.rs), [src/alloc.rs](../src/alloc.rs), [src/stun.rs](../src/stun.rs), [src/auth.rs](../src/auth.rs) -- TLS-Control-Plane ("turns") mit STUN-Framing: [src/tls.rs](../src/tls.rs) -- Test-Builder (wie Requests gebaut werden): [tests/support/stun_builders.rs](../tests/support/stun_builders.rs) - -## Begriffe - -- **Client**: TURN-Client (z.B. WebRTC ICE Agent) -- **Server**: `niom-turn` (dieses Projekt) -- **Peer**: Gegenstelle, zu der relayed werden soll (typischerweise anderer WebRTC-Endpunkt) -- **Allocation**: Server-seitige Sitzung, die ein **Relay-UDP-Socket** bereitstellt -- **Permission**: Erlaubnis, zu einem Peer zu senden/Peer-Pakete zu akzeptieren -- **Channel Binding**: Zuordnung `channel-number -> peer`, um ChannelData nutzen zu können - ---- - -## UDP: Sequenzgrafik (Happy Path) - -```mermaid -sequenceDiagram - autonumber - participant C as Client (TURN) - participant S as niom-turn (UDP:3478) - participant R as Relay Socket (UDP:ephemeral) - participant P as Peer - - Note over C,S: 1) Allocate ohne Auth → 401 Challenge - C->>S: STUN Allocate Request (ohne MI) - S->>C: STUN Error Response 401 + REALM + NONCE - - Note over C,S: 2) Allocate mit Long-Term Auth - C->>S: STUN Allocate Request + USERNAME/REALM/NONCE + MESSAGE-INTEGRITY - S->>R: bind("0.0.0.0:0"), spawn Relay-Loop - S->>C: Allocate Success + XOR-RELAYED-ADDRESS + LIFETIME (+ MESSAGE-INTEGRITY + FINGERPRINT) - - Note over C,S: 3) CreatePermission (Pflicht vor Send/ChannelBind) - C->>S: CreatePermission + XOR-PEER-ADDRESS (+ Auth + MI) - S->>C: Success (200) (+ MESSAGE-INTEGRITY + FINGERPRINT) - - Note over C,S: 4) Send (Client→Peer via Relay) - C->>S: Send + XOR-PEER-ADDRESS + DATA (+ Auth + MI) - S->>R: relay.send_to(DATA, Peer) - R->>P: UDP payload (source = relay_addr) - - Note over P,C: 5) Rückweg (Peer→Client) - P->>R: UDP payload (dest = relay_addr) - alt Channel Binding existiert - R->>S: recv_from(Peer) - S->>C: ChannelData(channel, payload) - else Kein Channel Binding - R->>S: recv_from(Peer) - S->>C: Data Indication (METHOD_DATA|INDICATION) + XOR-PEER-ADDRESS + DATA - end - - Note over C,S: 6) Optional: ChannelBind + ChannelData - C->>S: ChannelBind + CHANNEL-NUMBER + XOR-PEER-ADDRESS (+ Auth + MI) - S->>C: Success (200) (+ MESSAGE-INTEGRITY + FINGERPRINT) - C->>S: ChannelData(channel, payload) - S->>R: relay.send_to(payload, Peer) - R->>P: UDP payload - - Note over C,S: 7) Refresh - C->>S: Refresh + LIFETIME (+ Auth + MI) - S->>C: Success + LIFETIME(applied) (+ MESSAGE-INTEGRITY + FINGERPRINT) -``` - -### Was der Server dabei **genau** macht - -- Eingangspunkt: `udp_reader_loop` in [src/server.rs](../src/server.rs) -- Frühe Abzweigung: Wenn `parse_channel_data(...)` erfolgreich ist, wird **kein STUN** geparst, sondern ChannelData direkt weitergeleitet (nur wenn Allocation+Binding+Permission passt). -- STUN/TURN Requests werden mit `parse_message(...)` in [src/stun.rs](../src/stun.rs) geparst. - -### RFC-Interop Hinweis: FINGERPRINT - -- Alle vom Server gebauten STUN-Nachrichten enthalten `FINGERPRINT` als letztes Attribut. -- Wenn ein Client `FINGERPRINT` mitsendet, wird es validiert; bei ungültigem `FINGERPRINT` wird die Nachricht verworfen (kein Response). - -### Auth-Entscheidungen und typische Error-Codes - -Die Auth-Policy ist zentral in `AuthManager::authenticate` in [src/auth.rs](../src/auth.rs). - -- **401 Unauthorized**: Wenn `MESSAGE-INTEGRITY` fehlt → Challenge mit `REALM` + `NONCE`. -- **438 Stale Nonce**: Wenn `NONCE` abgelaufen/ungültig ist → neue Challenge. -- **437 Allocation Mismatch**: Wenn CreatePermission/Send/ChannelBind/Refresh ohne Allocation kommt. -- **403 Peer Not Permitted**: Wenn ein Peer nicht (mehr) permitted ist. -- **400 Missing/Invalid ...**: Wenn Attribute fehlen oder XOR-PEER-ADDRESS nicht dekodierbar ist. - ---- - -## TLS (turns): Sequenzgrafik (Control-Plane) - -Wichtig: Die TLS-Implementierung in [src/tls.rs](../src/tls.rs) nutzt denselben TURN-Handler wie TCP und implementiert eine echte **Stream-Data-Plane**. - -```mermaid -sequenceDiagram - autonumber - participant C as Client (turns) - participant T as niom-turn (TLS:5349) - participant R as Relay Socket (UDP:ephemeral) - participant P as Peer - - Note over C,T: STUN framing über TCP/TLS: read → chunk by (len+20) - C->>T: STUN Allocate (ohne MI) - T->>C: 401 + REALM + NONCE (über TLS) - - C->>T: STUN Allocate + Auth + MI - T->>R: allocate_for(peer, stream-sink) - T->>C: Allocate Success + XOR-RELAYED-ADDRESS + LIFETIME (über TLS) - - C->>T: CreatePermission/Send/Refresh/ChannelBind (über TLS) - T->>C: Success/Error (über TLS) - - Note over P,C: Peer-Daten kommen über den TLS-Stream zurück - P->>R: UDP payload an relay_addr - R->>T: recv_from(Peer) - T->>C: Data Indication / ChannelData (über TLS) -``` - -### Konsequenz - -- Control-Plane über TLS funktioniert (Allocate/Refresh/… werden über TLS beantwortet). -- Der **Data-Plane Rückweg** (Peer → Client) läuft ebenfalls über den TLS-Stream (Relay → `ClientSink::Stream`). - -Mehr Details dazu: [docs/tcp_tls_data_plane.md](tcp_tls_data_plane.md) - ---- - -## Mini-Checkliste: Minimaler Ablauf (praktisch) - -1. `ALLOCATE` ohne MI → `401` + `REALM` + `NONCE` -2. `ALLOCATE` mit `USERNAME/REALM/NONCE` + `MESSAGE-INTEGRITY` → `XOR-RELAYED-ADDRESS` + `LIFETIME` -3. `CREATE_PERMISSION` für Peer → `200` -4. `SEND` mit `DATA` → Server sendet via Relay an Peer -5. Peer sendet zurück an Relay → Server liefert an Client als `DATA-INDICATION` oder `CHANNEL-DATA` -6. Optional `CHANNEL_BIND` + ChannelData für effizientere Data-Plane -7. `REFRESH` zum Verlängern oder `LIFETIME=0` zum Freigeben diff --git a/docs/turn_rest_credentials.md b/docs/turn_rest_credentials.md deleted file mode 100644 index a46df6a..0000000 --- a/docs/turn_rest_credentials.md +++ /dev/null @@ -1,75 +0,0 @@ -# TURN REST Credentials (Ephemeral) – Nutzung & Betrieb - -Dieses Dokument erklärt die **TURN REST Credential** Strategie für `niom-turn` (MVP → production-fähiger Pfad). - -## Warum TURN REST? - -- Du willst für WebRTC *kurzlebige* TURN-Logins ausstellen (z.B. 5–10 Minuten). -- Dein TURN-Server speichert keine User-Passwörter pro Nutzer. -- Dein Backend (später) kann Tokens ausstellen; bis dahin kannst du lokal/ops-seitig Tokens generieren. - -## Verfahren (kompatibel zu gängigen WebRTC-Stacks) - -**Username**: `` oder `:` - -- Beispiel: `1735412345:alice` -- `expiry_unix_seconds` ist ein Unix-Timestamp in Sekunden. - -**Credential (Password)**: - -$$\n\text{credential} = \text{base64}(\text{HMAC-SHA1}(\text{secret}, \text{username}))\n$$ - -Dieses `credential` wird dann ganz normal als TURN-Passwort im ICE-Server-Config verwendet. - -## Server-Seite (`niom-turn`) - -Konfiguration in [appsettings.example.json](../appsettings.example.json): - -- `auth.rest_secret`: Shared Secret (muss geheim bleiben) -- `auth.rest_max_ttl_seconds`: Maximal akzeptiertes Zeitfenster in die Zukunft. Wenn ein Token zu weit in der Zukunft liegt, wird es abgewiesen (Sicherheitsmaßnahme). - -Wichtig: -- Der Server akzeptiert TURN REST Credentials als Fallback **nur wenn** der Username nicht im CredentialStore gefunden wird. -- Ablauf/TTL: - - `expiry` muss >= `now` sein - - und `expiry - now <= rest_max_ttl_seconds` - -## Lokal Tokens generieren (ohne Backend) - -Das Repo enthält ein kleines CLI: - -- Binary: [src/bin/turn_rest_cred.rs](../src/bin/turn_rest_cred.rs) - -Beispiele: - -1) Einfach (stdout als ENV-Zeilen) - -`cargo run --bin turn_rest_cred -- --secret "SUPER_SECRET" --user alice --ttl 600` - -2) JSON Ausgabe - -`cargo run --bin turn_rest_cred -- --secret "SUPER_SECRET" --user alice --ttl 600 --json` - -Du erhältst: -- `username` → in WebRTC als `iceServers[].username` -- `credential` → in WebRTC als `iceServers[].credential` - -## WebRTC Beispiel (Frontend) - -Du setzt in deiner App typischerweise: - -- URLs (mindestens UDP + TLS): - - `turn:your-domain:3478?transport=udp` - - `turn:your-domain:3478?transport=tcp` - - `turns:your-domain:5349?transport=tcp` - -und dann: -- `username`: vom Generator/Backend -- `credential`: vom Generator/Backend - -## Betriebshinweise - -- **Secret Rotation**: Plane Rotation (z.B. zwei Secrets parallel) – sonst brechen Tokens im Umlauf. -- **TTL klein halten**: 5–10 Minuten ist typisch. -- **Logs**: Niemals `secret` oder vollständige Credentials loggen. -- **Rate Limits/Quotas**: Unbedingt ergänzen (Open-Relay/Abuse vermeiden).