From 7169ed0d1e0fcc816d92d1cc2727d16ac6422b38 Mon Sep 17 00:00:00 2001 From: ghost Date: Sun, 16 Nov 2025 18:51:13 +0100 Subject: [PATCH] Introduced AuthManager with signed nonce handling and long-term credential validation. --- README.md | 38 +- appsettings.example.json | 7 +- src/auth.rs | 215 ++++++++ src/config.rs | 34 ++ src/main.rs | 709 ++++++++++++------------ src/stun.rs | 38 +- src/tls.rs | 1126 +++++++++++++++++++------------------- 7 files changed, 1216 insertions(+), 951 deletions(-) diff --git a/README.md b/README.md index 7d05f5c..10f351b 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,22 @@ Goals - Start with a minimal, well-tested parsing/utility layer and an in-memory credential store interface that can be replaced later. Current status -- UDP listener on 0.0.0.0:3478 (STUN/TURN) implemented. +- 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`. -- CredentialStore trait + in-memory implementation in `src/auth.rs`. -- Minimal logic: on any STUN request, server replies with a 401 challenge (REALM + NONCE). +- Optional TLS listener (0.0.0.0:5349) mirrors the UDP path for `turns:` clients. Design - Modules - - `stun.rs` - STUN/TURN message parsing and builders. - - `auth.rs` - CredentialStore trait and an `InMemoryStore` impl. Use the trait to swap for DB-backed stores later. - - `main.rs` - Bootstraps UDP listener, parses requests, and emits challenges for auth. + - `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. -CredentialStore interface -- `CredentialStore` is an async trait with `get_password(username) -> Option`. -- The default `InMemoryStore` is provided for tests and local dev. Swap in a production store by implementing the trait. +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 @@ -51,11 +53,10 @@ Security / Deployment - Ensure UDP and TCP/TLS ports (3478/5349) are reachable from the internet when used as a public TURN server. Auth caveat -- The current in-repo long-term auth implementation is intentionally minimal for the MVP and - uses legacy constructs (A1/MD5 derivation + HMAC-SHA1 MESSAGE-INTEGRITY). MD5 is not recommended - for new secure systems — this is present for RFC compatibility and testing only. We will replace - this with a secure credential workflow (ephemeral/REST credentials, PBKDF/KDF storage, or mTLS) - before any production deployment. See `src/auth.rs` for the current simple store and helpers. +- 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 ------------------------------ @@ -154,11 +155,16 @@ Das Projekt kann eine JSON-Konfigdatei `appsettings.json` im Arbeitsverzeichnis "username": "testuser", "password": "secretpassword" } - ] + ], + "auth": { + "realm": "niom-turn.local", + "nonce_secret": null, + "nonce_ttl_seconds": 300 + } } ``` -Wenn `appsettings.json` vorhanden ist, verwendet der Server die `server.bind` Adresse und befüllt den anfänglichen Credential-Store aus dem `credentials`-Array. Falls die Datei fehlt, verwendet der Server die internen Defaults (Bind `0.0.0.0:3478` und Demo-Cred `testuser`). +Wenn `appsettings.json` vorhanden ist, verwendet der Server die `server.bind` Adresse, befüllt den Credential-Store aus dem `credentials`-Array und übernimmt zusätzlich Realm/Nonce-Einstellungen aus `auth`. Falls die Datei fehlt, verwendet der Server die internen Defaults (Bind `0.0.0.0:3478`, Demo-Cred `testuser`, Realm `niom-turn.local`). Deployment & TLS / Long-term Auth roadmap ----------------------------------------- diff --git a/appsettings.example.json b/appsettings.example.json index 98637e5..1db6abe 100644 --- a/appsettings.example.json +++ b/appsettings.example.json @@ -9,5 +9,10 @@ "username": "testuser", "password": "secretpassword" } - ] + ], + "auth": { + "realm": "niom-turn.local", + "nonce_secret": null, + "nonce_ttl_seconds": 300 + } } diff --git a/src/auth.rs b/src/auth.rs index 33fcf18..e247d39 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,8 +1,16 @@ //! Authentication helpers and the in-memory credential store used for the MVP server. //! Backlog: Argon2-backed storage, nonce lifecycle, and integration with persistent secrets. +use crate::config::AuthOptions; +use crate::constants::{ATTR_NONCE, ATTR_REALM, ATTR_USERNAME}; +use crate::models::stun::StunMessage; +use crate::stun::{find_message_integrity, validate_message_integrity}; use crate::traits::CredentialStore; use async_trait::async_trait; +use hmac::{Hmac, Mac}; +use sha1::Sha1; +use std::net::SocketAddr; use std::sync::Arc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// Simple in-memory credential store for MVP #[derive(Clone, Default)] @@ -32,6 +40,213 @@ impl CredentialStore for InMemoryStore { } } +/// Authentication settings resolved from configuration for runtime usage. +#[derive(Clone, Debug)] +pub struct AuthSettings { + pub realm: String, + pub nonce_secret: Vec, + pub nonce_ttl: Duration, +} + +impl AuthSettings { + pub fn from_options(opts: &AuthOptions) -> Self { + let secret = opts + .nonce_secret + .clone() + .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); + // Ensure TTL does not collapse to zero so challenges stay valid briefly. + let ttl = Duration::from_secs(opts.nonce_ttl_seconds.max(60)); + Self { + realm: opts.realm.clone(), + nonce_secret: secret.into_bytes(), + nonce_ttl: ttl, + } + } +} + +/// Result of validating authentication attributes on an incoming STUN/TURN request. +#[derive(Debug, Clone)] +pub enum AuthStatus { + Granted { username: String }, + Challenge { nonce: String }, + StaleNonce { nonce: String }, + Reject { code: u16, reason: &'static str }, +} + +/// Orchestrates STUN/TURN long-term credential validation for the server. +pub struct AuthManager { + store: S, + settings: AuthSettings, +} + +impl Clone for AuthManager { + fn clone(&self) -> Self { + Self { + store: self.store.clone(), + settings: self.settings.clone(), + } + } +} + +impl AuthManager { + pub fn new(store: S, opts: &AuthOptions) -> Self { + Self { + store, + settings: AuthSettings::from_options(opts), + } + } + + pub fn realm(&self) -> &str { + &self.settings.realm + } + + /// Inspect a parsed STUN/TURN message and determine whether credentials are acceptable. + pub async fn authenticate(&self, msg: &StunMessage, peer: &SocketAddr) -> AuthStatus { + if find_message_integrity(msg).is_none() { + // Client has not yet computed MESSAGE-INTEGRITY; ask it to retry with credentials. + return AuthStatus::Challenge { + nonce: self.mint_nonce(peer), + }; + } + + let username = match self.attribute_utf8(msg, ATTR_USERNAME) { + Some(u) => u, + None => { + return AuthStatus::Challenge { + nonce: self.mint_nonce(peer), + } + } + }; + + let realm = match self.attribute_utf8(msg, ATTR_REALM) { + Some(r) => r, + None => { + return AuthStatus::Challenge { + nonce: self.mint_nonce(peer), + } + } + }; + + if realm != self.settings.realm { + return AuthStatus::Reject { + code: 400, + reason: "Realm Mismatch", + }; + } + + let nonce = match self.attribute_utf8(msg, ATTR_NONCE) { + Some(n) => n, + None => { + return AuthStatus::Challenge { + nonce: self.mint_nonce(peer), + } + } + }; + + match self.check_nonce(&nonce, peer) { + NonceValidation::Valid => {} + NonceValidation::Expired => { + return AuthStatus::StaleNonce { + nonce: self.mint_nonce(peer), + } + } + NonceValidation::Invalid => { + return AuthStatus::Challenge { + nonce: self.mint_nonce(peer), + } + } + } + + let password = match self.store.get_password(&username).await { + Some(p) => p, + None => { + return AuthStatus::Reject { + code: 401, + reason: "Unknown User", + } + } + }; + + let key = self.derive_long_term_key(&username, &password); + if !validate_message_integrity(msg, &key) { + return AuthStatus::Reject { + code: 401, + reason: "Bad Credentials", + }; + } + + AuthStatus::Granted { username } + } + + fn attribute_utf8(&self, msg: &StunMessage, attr_type: u16) -> Option { + msg.attributes + .iter() + .find(|a| a.typ == attr_type) + .and_then(|attr| std::str::from_utf8(&attr.value).ok()) + .map(|s| s.to_string()) + } + + fn derive_long_term_key(&self, username: &str, password: &str) -> Vec { + compute_a1_md5(username, &self.settings.realm, password) + } + + pub fn mint_nonce(&self, peer: &SocketAddr) -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)) + .as_secs(); + let payload = format!("{}|{}", now, peer.ip()); + let sig = self.sign_payload(payload.as_bytes()); + format!("{}:{}", now, sig) + } + + fn check_nonce(&self, nonce: &str, peer: &SocketAddr) -> NonceValidation { + let mut parts = nonce.splitn(2, ':'); + let ts_str = parts.next(); + let sig_str = parts.next(); + let (ts_str, sig_str) = match (ts_str, sig_str) { + (Some(ts), Some(sig)) => (ts, sig), + _ => return NonceValidation::Invalid, + }; + + let timestamp = match ts_str.parse::() { + Ok(t) => t, + Err(_) => return NonceValidation::Invalid, + }; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)) + .as_secs(); + if now.saturating_sub(timestamp) > self.settings.nonce_ttl.as_secs() { + return NonceValidation::Expired; + } + + let payload = format!("{}|{}", timestamp, peer.ip()); + let expected = self.sign_payload(payload.as_bytes()); + if expected == sig_str { + NonceValidation::Valid + } else { + NonceValidation::Invalid + } + } + + fn sign_payload(&self, payload: &[u8]) -> String { + type HmacSha1 = Hmac; + let mut mac = HmacSha1::new_from_slice(&self.settings.nonce_secret) + .expect("nonce secret to build hmac"); + mac.update(payload); + let bytes = mac.finalize().into_bytes(); + hex::encode(bytes) + } +} + +enum NonceValidation { + Valid, + Expired, + Invalid, +} + /// Helper: compute MESSAGE-INTEGRITY (HMAC-SHA1 as bytes) pub fn compute_hmac_sha1_bytes(key: &str, data: &[u8]) -> Vec { use hmac::{Hmac, Mac}; diff --git a/src/config.rs b/src/config.rs index c18111b..d199edb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,12 +3,43 @@ use serde::Deserialize; use std::path::Path; +fn default_realm() -> String { + "niom-turn.local".to_string() +} + +fn default_nonce_ttl_seconds() -> u64 { + 300 +} + #[derive(Debug, Deserialize, Clone)] pub struct CredentialEntry { pub username: String, pub password: String, } +#[derive(Debug, Deserialize, Clone)] +pub struct AuthOptions { + /// STUN/TURN realm advertised to clients when issuing challenges. + #[serde(default = "default_realm")] + pub realm: String, + /// Optional shared secret used to sign nonces; if omitted a random value is generated at runtime. + #[serde(default)] + pub nonce_secret: Option, + /// Validity period for generated nonces in seconds. + #[serde(default = "default_nonce_ttl_seconds")] + pub nonce_ttl_seconds: u64, +} + +impl Default for AuthOptions { + fn default() -> Self { + Self { + realm: default_realm(), + nonce_secret: None, + nonce_ttl_seconds: default_nonce_ttl_seconds(), + } + } +} + #[derive(Debug, Deserialize, Clone)] pub struct ServerOptions { /// Listen address, e.g. "0.0.0.0:3478" @@ -25,6 +56,9 @@ pub struct Config { /// Initial credentials to populate the credential store #[serde(default)] pub credentials: Vec, + /// Authentication behaviour advertised to clients. + #[serde(default)] + pub auth: AuthOptions, } impl Config { diff --git a/src/main.rs b/src/main.rs index 66d23a4..b863215 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,14 +7,13 @@ use tracing::{error, info}; // Use the library crate's public modules instead of local `mod` declarations. use niom_turn::alloc::AllocationManager; -use niom_turn::auth::InMemoryStore; -use niom_turn::config::Config; +use niom_turn::auth::{AuthManager, AuthStatus, InMemoryStore}; +use niom_turn::config::{AuthOptions, Config}; use niom_turn::constants::*; use niom_turn::stun::{ build_401_response, build_error_response, build_success_response, decode_xor_peer_address, - encode_xor_relayed_address, find_message_integrity, parse_message, validate_message_integrity, + encode_xor_relayed_address, parse_message, }; -use niom_turn::traits::CredentialStore; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -44,6 +43,7 @@ async fn main() -> anyhow::Result<()> { username: "testuser".into(), password: "secretpassword".into(), }], + auth: AuthOptions::default(), } } }; @@ -56,6 +56,8 @@ async fn main() -> anyhow::Result<()> { creds.insert(&c.username, &c.password); } + let auth = AuthManager::new(creds.clone(), &cfg.auth); + // Bind the UDP socket that receives STUN/TURN traffic from WebRTC clients. let udp = UdpSocket::bind(bind_addr).await?; let udp = Arc::new(udp); @@ -65,10 +67,10 @@ async fn main() -> anyhow::Result<()> { // Spawn the asynchronous packet loop that handles all UDP requests. let udp_clone = udp.clone(); - let creds_clone = creds.clone(); + let auth_clone = auth.clone(); let alloc_clone = alloc_mgr.clone(); tokio::spawn(async move { - if let Err(e) = udp_reader_loop(udp_clone, creds_clone, alloc_clone).await { + if let Err(e) = udp_reader_loop(udp_clone, auth_clone, alloc_clone).await { error!("udp loop error: {:?}", e); } }); @@ -76,7 +78,7 @@ async fn main() -> anyhow::Result<()> { // Optionally start the TLS listener so `turns:` clients can connect via TCP/TLS. if let (Some(cert), Some(key)) = (cfg.server.tls_cert.clone(), cfg.server.tls_key.clone()) { let udp_for_tls = udp.clone(); - let creds_for_tls = creds.clone(); + let auth_for_tls = auth.clone(); let alloc_for_tls = alloc_mgr.clone(); tokio::spawn(async move { if let Err(e) = niom_turn::tls::serve_tls( @@ -84,7 +86,7 @@ async fn main() -> anyhow::Result<()> { &cert, &key, udp_for_tls, - creds_for_tls, + auth_for_tls, alloc_for_tls, ) .await @@ -102,7 +104,7 @@ async fn main() -> anyhow::Result<()> { async fn udp_reader_loop( udp: Arc, - creds: InMemoryStore, + auth: AuthManager, allocs: AllocationManager, ) -> anyhow::Result<()> { let mut buf = vec![0u8; 1500]; @@ -119,377 +121,346 @@ async fn udp_reader_loop( msg.header.msg_type, len ); - // Fast-path authenticated requests when MESSAGE-INTEGRITY can be validated. - if let Some(_mi_attr) = find_message_integrity(&msg) { - // For MVP we expect username attribute (USERNAME) to be present - let username_attr = msg.attributes.iter().find(|a| a.typ == ATTR_USERNAME); - if let Some(u) = username_attr { - if let Ok(username) = std::str::from_utf8(&u.value) { - // lookup password - let store = creds.clone(); - let pw = store.get_password(username).await; - if let Some(password) = pw { - let valid = validate_message_integrity(&msg, &password); - if valid { - tracing::info!("MI valid for user {}", username); - // Handle authenticated Allocate to mint a relay binding for the client. - if msg.header.msg_type == METHOD_ALLOCATE { - match allocs.allocate_for(peer, udp.clone()).await { - Ok(relay_addr) => { - use bytes::BytesMut; - let mut out = BytesMut::new(); - let success_type = msg.header.msg_type | CLASS_SUCCESS; - out.extend_from_slice(&success_type.to_be_bytes()); - out.extend_from_slice(&0u16.to_be_bytes()); - out.extend_from_slice(&MAGIC_COOKIE_U32.to_be_bytes()); - out.extend_from_slice(&msg.header.transaction_id); - // RFC: XOR-RELAYED-ADDRESS (0x0016) - let attr_val = encode_xor_relayed_address( - &relay_addr, - &msg.header.transaction_id, - ); - out.extend_from_slice( - &ATTR_XOR_RELAYED_ADDRESS.to_be_bytes(), - ); - out.extend_from_slice( - &((attr_val.len() as u16).to_be_bytes()), - ); - out.extend_from_slice(&attr_val); - while (out.len() % 4) != 0 { - out.extend_from_slice(&[0]); - } - let total_len = (out.len() - 20) as u16; - let len_bytes = total_len.to_be_bytes(); - out[2] = len_bytes[0]; - out[3] = len_bytes[1]; - let vec_out = out.to_vec(); - tracing::info!("sending allocate success (mi-valid) -> {} bytes hex={} ", vec_out.len(), hex::encode(&vec_out)); - let _ = udp.send_to(&vec_out, &peer).await; - continue; - } - Err(e) => tracing::error!( - "allocate failed after MI valid: {:?}", - e - ), - } - } else if msg.header.msg_type == METHOD_CREATE_PERMISSION { - // Permission updates extend the list of peer addresses an allocation may forward to. - if allocs.get_allocation(&peer).is_none() { - tracing::warn!( - "create-permission without allocation from {}", - peer - ); - let resp = build_error_response( - &msg.header, - 437, - "Allocation Mismatch", - ); - let _ = udp.send_to(&resp, &peer).await; - continue; - } + let requires_auth = matches!( + msg.header.msg_type, + METHOD_ALLOCATE + | METHOD_CREATE_PERMISSION + | METHOD_CHANNEL_BIND + | METHOD_SEND + | METHOD_REFRESH + ); - let mut added = 0usize; - for attr in msg - .attributes - .iter() - .filter(|a| a.typ == ATTR_XOR_PEER_ADDRESS) - { - if let Some(peer_addr) = decode_xor_peer_address( - &attr.value, - &msg.header.transaction_id, - ) { - match allocs.add_permission(peer, peer_addr) { - Ok(()) => { - tracing::info!( - "added permission for {} -> {}", - peer, - peer_addr - ); - added += 1; - } - Err(e) => { - tracing::error!("failed to persist permission {} -> {}: {:?}", peer, peer_addr, e); - } - } - } else { - tracing::warn!( - "invalid XOR-PEER-ADDRESS in request from {}", - peer - ); - } - } + if requires_auth { + match auth.authenticate(&msg, &peer).await { + AuthStatus::Granted { username } => { + tracing::debug!( + "TURN auth ok for {} as {} (0x{:04x})", + peer, + username, + msg.header.msg_type + ); + } + AuthStatus::Challenge { nonce } => { + let resp = build_401_response( + &msg.header, + auth.realm(), + &nonce, + 401, + "Unauthorized", + ); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + AuthStatus::StaleNonce { nonce } => { + let resp = build_401_response( + &msg.header, + auth.realm(), + &nonce, + 438, + "Stale Nonce", + ); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + AuthStatus::Reject { code, reason } => { + let resp = build_error_response(&msg.header, code, reason); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + } - if added == 0 { - let resp = build_error_response( - &msg.header, - 400, - "No valid XOR-PEER-ADDRESS", - ); - let _ = udp.send_to(&resp, &peer).await; - } else { - let resp = build_success_response(&msg.header); - let _ = udp.send_to(&resp, &peer).await; - } - continue; - } else if msg.header.msg_type == METHOD_CHANNEL_BIND { - let allocation = match allocs.get_allocation(&peer) { - Some(a) => a, - None => { - tracing::warn!( - "channel-bind without allocation from {}", - peer - ); - let resp = build_error_response( - &msg.header, - 437, - "Allocation Mismatch", - ); - let _ = udp.send_to(&resp, &peer).await; - continue; - } - }; - - let channel_attr = msg - .attributes - .iter() - .find(|a| a.typ == ATTR_CHANNEL_NUMBER); - let peer_attr = msg - .attributes - .iter() - .find(|a| a.typ == ATTR_XOR_PEER_ADDRESS); - - let channel = match channel_attr.and_then(|attr| { - if attr.value.len() >= 4 { - Some(u16::from_be_bytes([attr.value[0], attr.value[1]])) - } else { - None - } - }) { - Some(c) => c, - None => { - let resp = build_error_response( - &msg.header, - 400, - "Missing CHANNEL-NUMBER", - ); - let _ = udp.send_to(&resp, &peer).await; - continue; - } - }; - - if channel < 0x4000 || channel > 0x7FFF { - let resp = build_error_response( - &msg.header, - 400, - "Channel Out Of Range", - ); - let _ = udp.send_to(&resp, &peer).await; - continue; - } - - let peer_addr = match peer_attr.and_then(|attr| { - decode_xor_peer_address( - &attr.value, - &msg.header.transaction_id, - ) - }) { - Some(addr) => addr, - None => { - let resp = build_error_response( - &msg.header, - 400, - "Missing XOR-PEER-ADDRESS", - ); - let _ = udp.send_to(&resp, &peer).await; - continue; - } - }; - - if !allocation.is_peer_allowed(&peer_addr) { - let resp = build_error_response( - &msg.header, - 403, - "Peer Not Permitted", - ); - let _ = udp.send_to(&resp, &peer).await; - continue; - } - - match allocs.add_channel_binding(peer, channel, peer_addr) { - Ok(()) => { - tracing::info!( - "bound channel 0x{:04x} for {} -> {}", - channel, - peer, - peer_addr - ); - let resp = build_success_response(&msg.header); - let _ = udp.send_to(&resp, &peer).await; - } - Err(e) => { - tracing::error!( - "failed to add channel binding {} -> {} (channel 0x{:04x}): {:?}", - peer, peer_addr, channel, e - ); - let resp = build_error_response( - &msg.header, - 500, - "Channel Binding Failed", - ); - let _ = udp.send_to(&resp, &peer).await; - } - } - continue; - } else if msg.header.msg_type == METHOD_SEND { - let allocation = match allocs.get_allocation(&peer) { - Some(a) => a, - None => { - tracing::warn!("send without allocation from {}", peer); - let resp = build_error_response( - &msg.header, - 437, - "Allocation Mismatch", - ); - let _ = udp.send_to(&resp, &peer).await; - continue; - } - }; - - let peer_attr = msg - .attributes - .iter() - .find(|a| a.typ == ATTR_XOR_PEER_ADDRESS); - let data_attr = - msg.attributes.iter().find(|a| a.typ == ATTR_DATA); - - let peer_addr = match peer_attr.and_then(|attr| { - decode_xor_peer_address( - &attr.value, - &msg.header.transaction_id, - ) - }) { - Some(addr) => addr, - None => { - let resp = build_error_response( - &msg.header, - 400, - "Missing XOR-PEER-ADDRESS", - ); - let _ = udp.send_to(&resp, &peer).await; - continue; - } - }; - - let data_attr = match data_attr { - Some(attr) => attr, - None => { - let resp = build_error_response( - &msg.header, - 400, - "Missing DATA Attribute", - ); - let _ = udp.send_to(&resp, &peer).await; - continue; - } - }; - - if !allocation.is_peer_allowed(&peer_addr) { - let resp = build_error_response( - &msg.header, - 403, - "Peer Not Permitted", - ); - let _ = udp.send_to(&resp, &peer).await; - continue; - } - - match allocation.send_to_peer(peer_addr, &data_attr.value).await - { - Ok(sent) => { - tracing::info!( - "forwarded {} bytes from {} to peer {}", - sent, - peer, - peer_addr - ); - let resp = build_success_response(&msg.header); - let _ = udp.send_to(&resp, &peer).await; - } - Err(e) => { - tracing::error!( - "failed to send payload from {} to {}: {:?}", - peer, - peer_addr, - e - ); - let resp = build_error_response( - &msg.header, - 500, - "Peer Send Failed", - ); - let _ = udp.send_to(&resp, &peer).await; - } - } - continue; + match msg.header.msg_type { + METHOD_ALLOCATE => { + use bytes::BytesMut; + match allocs.allocate_for(peer, udp.clone()).await { + Ok(relay_addr) => { + let mut out = BytesMut::new(); + let success_type = msg.header.msg_type | CLASS_SUCCESS; + out.extend_from_slice(&success_type.to_be_bytes()); + out.extend_from_slice(&0u16.to_be_bytes()); + out.extend_from_slice(&MAGIC_COOKIE_U32.to_be_bytes()); + out.extend_from_slice(&msg.header.transaction_id); + let attr_val = encode_xor_relayed_address( + &relay_addr, + &msg.header.transaction_id, + ); + out.extend_from_slice(&ATTR_XOR_RELAYED_ADDRESS.to_be_bytes()); + out.extend_from_slice(&((attr_val.len() as u16).to_be_bytes())); + out.extend_from_slice(&attr_val); + while (out.len() % 4) != 0 { + out.extend_from_slice(&[0]); } - // Non-specific success path: echo a success response so the client continues handshake. - let resp = build_success_response(&msg.header); + let total_len = (out.len() - 20) as u16; + let len_bytes = total_len.to_be_bytes(); + out[2] = len_bytes[0]; + out[3] = len_bytes[1]; + let vec_out = out.to_vec(); + tracing::info!( + "sending allocate success -> {} bytes hex={} ", + vec_out.len(), + hex::encode(&vec_out) + ); + let _ = udp.send_to(&vec_out, &peer).await; + } + Err(e) => { + tracing::error!("allocate failed: {:?}", e); + let resp = + build_error_response(&msg.header, 500, "Allocate Failed"); + let _ = udp.send_to(&resp, &peer).await; + } + } + continue; + } + METHOD_CREATE_PERMISSION => { + if allocs.get_allocation(&peer).is_none() { + tracing::warn!("create-permission without allocation from {}", peer); + let resp = + build_error_response(&msg.header, 437, "Allocation Mismatch"); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + + let mut added = 0usize; + for attr in msg + .attributes + .iter() + .filter(|a| a.typ == ATTR_XOR_PEER_ADDRESS) + { + if let Some(peer_addr) = + decode_xor_peer_address(&attr.value, &msg.header.transaction_id) + { + match allocs.add_permission(peer, peer_addr) { + Ok(()) => { + tracing::info!( + "added permission for {} -> {}", + peer, + peer_addr + ); + added += 1; + } + Err(e) => { + tracing::error!( + "failed to persist permission {} -> {}: {:?}", + peer, + peer_addr, + e + ); + } + } + } else { + tracing::warn!("invalid XOR-PEER-ADDRESS in request from {}", peer); + } + } + + if added == 0 { + let resp = + build_error_response(&msg.header, 400, "No valid XOR-PEER-ADDRESS"); + let _ = udp.send_to(&resp, &peer).await; + } else { + let resp = build_success_response(&msg.header); + let _ = udp.send_to(&resp, &peer).await; + } + continue; + } + METHOD_CHANNEL_BIND => { + let allocation = match allocs.get_allocation(&peer) { + Some(a) => a, + None => { + tracing::warn!("channel-bind without allocation from {}", peer); + let resp = + build_error_response(&msg.header, 437, "Allocation Mismatch"); let _ = udp.send_to(&resp, &peer).await; continue; - } else { - tracing::info!("MI invalid for user {}", username); } - } else { - tracing::info!("unknown user {}", username); + }; + + let channel_attr = + msg.attributes.iter().find(|a| a.typ == ATTR_CHANNEL_NUMBER); + let peer_attr = msg + .attributes + .iter() + .find(|a| a.typ == ATTR_XOR_PEER_ADDRESS); + + let channel = match channel_attr.and_then(|attr| { + if attr.value.len() >= 4 { + Some(u16::from_be_bytes([attr.value[0], attr.value[1]])) + } else { + None + } + }) { + Some(c) => c, + None => { + let resp = build_error_response( + &msg.header, + 400, + "Missing CHANNEL-NUMBER", + ); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + }; + + if channel < 0x4000 || channel > 0x7FFF { + let resp = + build_error_response(&msg.header, 400, "Channel Out Of Range"); + let _ = udp.send_to(&resp, &peer).await; + continue; } + + let peer_addr = match peer_attr.and_then(|attr| { + decode_xor_peer_address(&attr.value, &msg.header.transaction_id) + }) { + Some(addr) => addr, + None => { + let resp = build_error_response( + &msg.header, + 400, + "Missing XOR-PEER-ADDRESS", + ); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + }; + + if !allocation.is_peer_allowed(&peer_addr) { + let resp = build_error_response(&msg.header, 403, "Peer Not Permitted"); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + + match allocs.add_channel_binding(peer, channel, peer_addr) { + Ok(()) => { + tracing::info!( + "bound channel 0x{:04x} for {} -> {}", + channel, + peer, + peer_addr + ); + let resp = build_success_response(&msg.header); + let _ = udp.send_to(&resp, &peer).await; + } + Err(e) => { + tracing::error!( + "failed to add channel binding {} -> {} (channel 0x{:04x}): {:?}", + peer, + peer_addr, + channel, + e + ); + let resp = build_error_response( + &msg.header, + 500, + "Channel Binding Failed", + ); + let _ = udp.send_to(&resp, &peer).await; + } + } + continue; + } + METHOD_SEND => { + let allocation = match allocs.get_allocation(&peer) { + Some(a) => a, + None => { + tracing::warn!("send without allocation from {}", peer); + let resp = + build_error_response(&msg.header, 437, "Allocation Mismatch"); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + }; + + let peer_attr = msg + .attributes + .iter() + .find(|a| a.typ == ATTR_XOR_PEER_ADDRESS); + let data_attr = msg.attributes.iter().find(|a| a.typ == ATTR_DATA); + + let peer_addr = match peer_attr.and_then(|attr| { + decode_xor_peer_address(&attr.value, &msg.header.transaction_id) + }) { + Some(addr) => addr, + None => { + let resp = build_error_response( + &msg.header, + 400, + "Missing XOR-PEER-ADDRESS", + ); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + }; + + let data_attr = match data_attr { + Some(attr) => attr, + None => { + let resp = build_error_response( + &msg.header, + 400, + "Missing DATA Attribute", + ); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + }; + + if !allocation.is_peer_allowed(&peer_addr) { + let resp = build_error_response(&msg.header, 403, "Peer Not Permitted"); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + + match allocation.send_to_peer(peer_addr, &data_attr.value).await { + Ok(sent) => { + tracing::info!( + "forwarded {} bytes from {} to peer {}", + sent, + peer, + peer_addr + ); + let resp = build_success_response(&msg.header); + let _ = udp.send_to(&resp, &peer).await; + } + Err(e) => { + tracing::error!( + "failed to send payload from {} to {}: {:?}", + peer, + peer_addr, + e + ); + let resp = + build_error_response(&msg.header, 500, "Peer Send Failed"); + let _ = udp.send_to(&resp, &peer).await; + } + } + continue; + } + METHOD_REFRESH => { + // Refresh support is still MVP-level; acknowledge so clients extend allocations. + let resp = build_success_response(&msg.header); + let _ = udp.send_to(&resp, &peer).await; + continue; + } + _ => { + let resp = build_error_response(&msg.header, 420, "Unknown TURN Method"); + let _ = udp.send_to(&resp, &peer).await; + continue; } } } - // Allow unauthenticated Allocate to fall back to challenge/early success for now (MVP compatibility). - if msg.header.msg_type == METHOD_ALLOCATE { - // If we reach here without MI, still attempt allocation but we will send a 401 earlier - let relay = allocs.allocate_for(peer, udp.clone()).await; - match relay { - Ok(relay_addr) => { - use bytes::BytesMut; - let mut out = BytesMut::new(); - let success_type = msg.header.msg_type | CLASS_SUCCESS; - out.extend_from_slice(&success_type.to_be_bytes()); - out.extend_from_slice(&0u16.to_be_bytes()); - out.extend_from_slice(&MAGIC_COOKIE_U32.to_be_bytes()); - out.extend_from_slice(&msg.header.transaction_id); - let attr_val = - encode_xor_relayed_address(&relay_addr, &msg.header.transaction_id); - out.extend_from_slice(&ATTR_XOR_RELAYED_ADDRESS.to_be_bytes()); - out.extend_from_slice(&((attr_val.len() as u16).to_be_bytes())); - out.extend_from_slice(&attr_val); - while (out.len() % 4) != 0 { - out.extend_from_slice(&[0]); - } - let total_len = (out.len() - 20) as u16; - let len_bytes = total_len.to_be_bytes(); - out[2] = len_bytes[0]; - out[3] = len_bytes[1]; - let vec_out = out.to_vec(); - tracing::info!( - "sending allocate success (no-mi) -> {} bytes hex={} ", - vec_out.len(), - hex::encode(&vec_out) - ); - let _ = udp.send_to(&vec_out, &peer).await; - } - Err(e) => { - error!("allocate failed: {:?}", e); - } - } - continue; - } - // Everything else receives a 401 challenge so the client can retry with credentials. - let nonce = format!("nonce-{}", uuid::Uuid::new_v4()); - let resp = build_401_response(&msg.header, "niom-turn.local", &nonce, 401); - if let Err(e) = udp.send_to(&resp, &peer).await { - error!("failed to send 401: {:?}", e); + match msg.header.msg_type { + METHOD_BINDING => { + let resp = build_success_response(&msg.header); + let _ = udp.send_to(&resp, &peer).await; + } + _ => { + let nonce = auth.mint_nonce(&peer); + let resp = + build_401_response(&msg.header, auth.realm(), &nonce, 401, "Unauthorized"); + if let Err(e) = udp.send_to(&resp, &peer).await { + error!("failed to send 401: {:?}", e); + } + } } } else { tracing::debug!("Non-STUN or parse error from {} len={}", peer, len); diff --git a/src/stun.rs b/src/stun.rs index a78f2cb..fc31d0f 100644 --- a/src/stun.rs +++ b/src/stun.rs @@ -66,8 +66,14 @@ pub fn parse_message(buf: &[u8]) -> Result { }) } -/// Build a minimal 401 error response (REALM + NONCE). Returns the bytes to send. -pub fn build_401_response(req: &StunHeader, realm: &str, nonce: &str, _err_code: u16) -> Vec { +/// Build a challenge error response with REALM + NONCE and the provided error code. +pub fn build_401_response( + req: &StunHeader, + realm: &str, + nonce: &str, + err_code: u16, + reason: &str, +) -> Vec { use bytes::BytesMut; let mut buf = BytesMut::new(); // Error response type for TURN: reuse the request method with error class bits set @@ -77,6 +83,20 @@ pub fn build_401_response(req: &StunHeader, realm: &str, nonce: &str, _err_code: buf.extend_from_slice(&MAGIC_COOKIE_BYTES); buf.extend_from_slice(&req.transaction_id); + // ERROR-CODE attribute (RFC 5389 section 15.6) + let mut err_value = Vec::new(); + let class = (err_code / 100) as u8; + let number = (err_code % 100) as u8; + err_value.extend_from_slice(&[0, 0, class, number]); + err_value.extend_from_slice(reason.as_bytes()); + + buf.extend_from_slice(&ATTR_ERROR_CODE.to_be_bytes()); + buf.extend_from_slice(&(err_value.len() as u16).to_be_bytes()); + buf.extend_from_slice(&err_value); + while (buf.len() % 4) != 0 { + buf.extend_from_slice(&[0]); + } + // REALM (RFC attr) let realm_bytes = realm.as_bytes(); buf.extend_from_slice(&ATTR_REALM.to_be_bytes()); @@ -147,7 +167,7 @@ pub fn find_message_integrity(msg: &StunMessage) -> Option<&StunAttribute> { /// Validate MESSAGE-INTEGRITY using provided key (password). Returns true if valid. /// Note: This is a simplified validator that assumes the MESSAGE-INTEGRITY attribute exists and /// that the message bytes passed are the full STUN message (including attributes). -pub fn validate_message_integrity(msg: &StunMessage, key: &str) -> bool { +pub fn validate_message_integrity(msg: &StunMessage, key: &[u8]) -> bool { if let Some(mi) = find_message_integrity(msg) { // MESSAGE-INTEGRITY attribute value is 20 bytes (HMAC-SHA1) if mi.value.len() != 20 { @@ -189,11 +209,11 @@ pub fn compute_fingerprint(msg: &[u8]) -> u32 { } /// Compute MESSAGE-INTEGRITY (HMAC-SHA1) over the message -pub fn compute_message_integrity(key: &str, msg: &[u8]) -> Vec { +pub fn compute_message_integrity(key: &[u8], msg: &[u8]) -> Vec { use hmac::{Hmac, Mac}; use sha1::Sha1; type HmacSha1 = Hmac; - let mut mac = HmacSha1::new_from_slice(key.as_bytes()).expect("HMAC key"); + let mut mac = HmacSha1::new_from_slice(key).expect("HMAC key"); mac.update(msg); mac.finalize().into_bytes().to_vec() } @@ -284,7 +304,7 @@ mod tests { cookie: MAGIC_COOKIE_U32, transaction_id: [2u8; 12], }; - let out = build_401_response(&req, "realm", "nonce", 401); + let out = build_401_response(&req, "realm", "nonce", 401, "Unauthorized"); // parse back should succeed let parsed = parse_message(&out).expect("parse resp"); assert!(!parsed.attributes.is_empty()); @@ -331,7 +351,7 @@ mod tests { buf[3] = len_bytes[1]; // Compute HMAC over message up to MI attribute header (mi_attr_offset) - let hmac = compute_message_integrity(password, &buf[..mi_attr_offset]); + let hmac = compute_message_integrity(password.as_bytes(), &buf[..mi_attr_offset]); // place first 20 bytes into mi value for i in 0..20 { buf[mi_val_pos + i] = hmac[i]; @@ -339,12 +359,12 @@ mod tests { // Parse and validate let parsed = parse_message(&buf).expect("parsed"); - assert!(validate_message_integrity(&parsed, password)); + assert!(validate_message_integrity(&parsed, password.as_bytes())); // tamper: change one byte -> invalid let mut tampered = buf.to_vec(); tampered[10] ^= 0xFF; let parsed2 = parse_message(&tampered).expect("parsed2"); - assert!(!validate_message_integrity(&parsed2, password)); + assert!(!validate_message_integrity(&parsed2, password.as_bytes())); } } diff --git a/src/tls.rs b/src/tls.rs index b610040..d068041 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -10,13 +10,12 @@ use tokio_rustls::rustls::{Certificate, PrivateKey, ServerConfig}; use tokio_rustls::TlsAcceptor; use crate::alloc::AllocationManager; -use crate::auth::InMemoryStore; +use crate::auth::{AuthManager, AuthStatus, InMemoryStore}; use crate::constants::*; use crate::stun::{ build_401_response, build_error_response, build_success_response, decode_xor_peer_address, - encode_xor_relayed_address, find_message_integrity, parse_message, validate_message_integrity, + encode_xor_relayed_address, parse_message, }; -use crate::traits::CredentialStore; fn load_certs(path: &str) -> anyhow::Result> { let f = File::open(path).context("opening cert file")?; @@ -49,7 +48,7 @@ pub async fn serve_tls( cert_path: &str, key_path: &str, udp_sock: std::sync::Arc, - creds: InMemoryStore, + auth: AuthManager, allocs: AllocationManager, ) -> anyhow::Result<()> { let certs = load_certs(cert_path)?; @@ -60,7 +59,6 @@ pub async fn serve_tls( .with_no_client_auth() .with_single_cert(certs, key)?; - // set protocols etc if needed let acceptor = TlsAcceptor::from(Arc::new(cfg)); let listener = TcpListener::bind(bind).await?; @@ -70,7 +68,7 @@ pub async fn serve_tls( let (stream, peer) = listener.accept().await?; let acceptor = acceptor.clone(); let udp_clone = udp_sock.clone(); - let creds_clone = creds.clone(); + let auth_clone = auth.clone(); let alloc_clone = allocs.clone(); tokio::spawn(async move { @@ -79,6 +77,7 @@ pub async fn serve_tls( tracing::info!("accepted TLS connection from {}", peer); let mut read_buf = vec![0u8; 4096]; let mut buffer: Vec = Vec::new(); + loop { match tls_stream.read(&mut read_buf).await { Ok(0) => { @@ -87,7 +86,6 @@ pub async fn serve_tls( } Ok(n) => { buffer.extend_from_slice(&read_buf[..n]); - // Try to extract STUN messages framed by header length while buffer.len() >= 20 { let len = u16::from_be_bytes([buffer[2], buffer[3]]) as usize; let total = len + 20; @@ -96,568 +94,584 @@ pub async fn serve_tls( } let chunk = buffer.drain(..total).collect::>(); if let Ok(msg) = parse_message(&chunk) { - // process message similarly to UDP path - if let Some(_mi_attr) = find_message_integrity(&msg) { - let username_attr = msg - .attributes - .iter() - .find(|a| a.typ == ATTR_USERNAME); - if let Some(u) = username_attr { - if let Ok(username) = std::str::from_utf8(&u.value) - { - let pw = - creds_clone.get_password(username).await; - if let Some(password) = pw { - let valid = validate_message_integrity( - &msg, &password, - ); - if valid { - tracing::info!( - "MI valid for user {} on TLS", - username - ); - if msg.header.msg_type - == METHOD_ALLOCATE - { - match alloc_clone.allocate_for(peer, udp_clone.clone()).await { - Ok(relay_addr) => { - let mut out = Vec::new(); - let success_type = msg.header.msg_type | CLASS_SUCCESS; - out.extend_from_slice(&success_type.to_be_bytes()); - out.extend_from_slice(&0u16.to_be_bytes()); - out.extend_from_slice(&MAGIC_COOKIE_BYTES); - out.extend_from_slice(&msg.header.transaction_id); - let attr_val = encode_xor_relayed_address(&relay_addr, &msg.header.transaction_id); - out.extend_from_slice(&ATTR_XOR_RELAYED_ADDRESS.to_be_bytes()); - out.extend_from_slice(&((attr_val.len() as u16).to_be_bytes())); - out.extend_from_slice(&attr_val); - while (out.len() % 4) != 0 { out.extend_from_slice(&[0]); } - let total_len = (out.len() - 20) as u16; - let len_bytes = total_len.to_be_bytes(); - out[2] = len_bytes[0]; out[3] = len_bytes[1]; - if let Err(e) = tls_stream.write_all(&out).await { - tracing::error!("failed to write tls response: {:?}", e); - } - continue; - } - Err(e) => tracing::error!("allocate failed after MI valid (tls): {:?}", e), - } - } else if msg.header.msg_type - == METHOD_CREATE_PERMISSION - { - if alloc_clone - .get_allocation(&peer) - .is_none() - { - tracing::warn!("create-permission without allocation from {} (tls)", peer); - let resp = build_error_response( - &msg.header, - 437, - "Allocation Mismatch", - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!("failed to write tls error: {:?}", e); - } - continue; - } + tracing::info!( + "STUN/TURN over TLS from {} type=0x{:04x} len={}", + peer, + msg.header.msg_type, + total + ); - let mut added = 0usize; - for attr in msg - .attributes - .iter() - .filter(|a| { - a.typ - == ATTR_XOR_PEER_ADDRESS - }) - { - if let Some(peer_addr) = - decode_xor_peer_address( - &attr.value, - &msg.header - .transaction_id, - ) - { - match alloc_clone.add_permission(peer, peer_addr) { - Ok(()) => { - tracing::info!("added TLS permission for {} -> {}", peer, peer_addr); - added += 1; - } - Err(e) => tracing::error!("failed to persist TLS permission {} -> {}: {:?}", peer, peer_addr, e), - } - } else { - tracing::warn!("invalid XOR-PEER-ADDRESS via TLS from {}", peer); - } - } + let requires_auth = matches!( + msg.header.msg_type, + METHOD_ALLOCATE + | METHOD_CREATE_PERMISSION + | METHOD_CHANNEL_BIND + | METHOD_SEND + | METHOD_REFRESH + ); - let resp = if added == 0 { - build_error_response( - &msg.header, - 400, - "No valid XOR-PEER-ADDRESS", - ) - } else { - build_success_response( - &msg.header, - ) - }; - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!("failed to write tls response: {:?}", e); - } - continue; - } else if msg.header.msg_type - == METHOD_CHANNEL_BIND - { - let allocation = match alloc_clone - .get_allocation(&peer) - { - Some(a) => a, - None => { - tracing::warn!( - "channel-bind without allocation from {} (tls)", - peer - ); - let resp = build_error_response( - &msg.header, - 437, - "Allocation Mismatch", - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e - ); - } - continue; - } - }; - - let channel_attr = msg - .attributes - .iter() - .find(|a| { - a.typ == ATTR_CHANNEL_NUMBER - }); - let peer_attr = msg - .attributes - .iter() - .find(|a| { - a.typ - == ATTR_XOR_PEER_ADDRESS - }); - - let channel = match channel_attr - .and_then(|attr| { - if attr.value.len() >= 4 { - Some( - u16::from_be_bytes( - [ - attr.value - [0], - attr.value - [1], - ], - ), - ) - } else { - None - } - }) { - Some(c) => c, - None => { - let resp = build_error_response( - &msg.header, - 400, - "Missing CHANNEL-NUMBER", - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e - ); - } - continue; - } - }; - - if channel < 0x4000 - || channel > 0x7FFF - { - let resp = build_error_response( - &msg.header, - 400, - "Channel Out Of Range", - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e - ); - } - continue; - } - - let peer_addr = match peer_attr - .and_then(|attr| { - decode_xor_peer_address( - &attr.value, - &msg.header - .transaction_id, - ) - }) { - Some(addr) => addr, - None => { - let resp = build_error_response( - &msg.header, - 400, - "Missing XOR-PEER-ADDRESS", - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e - ); - } - continue; - } - }; - - if !allocation - .is_peer_allowed(&peer_addr) - { - let resp = build_error_response( - &msg.header, - 403, - "Peer Not Permitted", - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e - ); - } - continue; - } - - match alloc_clone - .add_channel_binding( - peer, channel, peer_addr, - ) { - Ok(()) => { - tracing::info!( - "bound channel 0x{:04x} for {} -> {} over TLS", - channel, - peer, - peer_addr - ); - let resp = - build_success_response( - &msg.header, - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls response: {:?}", - e - ); - } - } - Err(e) => { - tracing::error!( - "failed TLS channel binding {} -> {} (0x{:04x}): {:?}", - peer, - peer_addr, - channel, - e - ); - let resp = build_error_response( - &msg.header, - 500, - "Channel Binding Failed", - ); - if let Err(e2) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e2 - ); - } - } - } - continue; - } else if msg.header.msg_type - == METHOD_SEND - { - let allocation = match alloc_clone - .get_allocation(&peer) - { - Some(a) => a, - None => { - tracing::warn!( - "send without allocation from {} (tls)", - peer - ); - let resp = build_error_response( - &msg.header, - 437, - "Allocation Mismatch", - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e - ); - } - continue; - } - }; - - let peer_attr = msg - .attributes - .iter() - .find(|a| { - a.typ - == ATTR_XOR_PEER_ADDRESS - }); - let data_attr = msg - .attributes - .iter() - .find(|a| a.typ == ATTR_DATA); - - let peer_addr = match peer_attr - .and_then(|attr| { - decode_xor_peer_address( - &attr.value, - &msg.header - .transaction_id, - ) - }) { - Some(addr) => addr, - None => { - let resp = build_error_response( - &msg.header, - 400, - "Missing XOR-PEER-ADDRESS", - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e - ); - } - continue; - } - }; - - let data_attr = match data_attr { - Some(attr) => attr, - None => { - let resp = build_error_response( - &msg.header, - 400, - "Missing DATA Attribute", - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e - ); - } - continue; - } - }; - - if !allocation - .is_peer_allowed(&peer_addr) - { - let resp = build_error_response( - &msg.header, - 403, - "Peer Not Permitted", - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e - ); - } - continue; - } - - match allocation - .send_to_peer( - peer_addr, - &data_attr.value, - ) - .await - { - Ok(sent) => { - tracing::info!( - "forwarded {} bytes from {} to {} via TLS session", - sent, - peer, - peer_addr - ); - let resp = - build_success_response( - &msg.header, - ); - if let Err(e) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls response: {:?}", - e - ); - } - } - Err(e) => { - tracing::error!( - "failed to send payload from {} to {} via TLS: {:?}", - peer, - peer_addr, - e - ); - let resp = - build_error_response( - &msg.header, - 500, - "Peer Send Failed", - ); - if let Err(e2) = tls_stream - .write_all(&resp) - .await - { - tracing::error!( - "failed to write tls error: {:?}", - e2 - ); - } - } - } - continue; - } - let resp = - build_success_response(&msg.header); - if let Err(e) = - tls_stream.write_all(&resp).await - { - tracing::error!("failed to write tls response: {:?}", e); - } - continue; - } else { - tracing::info!( - "MI invalid for user {} (tls)", - username - ); - } - } else { - tracing::info!( - "unknown user {} (tls)", - username - ); - } + if requires_auth { + match auth_clone.authenticate(&msg, &peer).await { + AuthStatus::Granted { username } => { + tracing::debug!( + "TURN TLS auth ok for {} as {} (0x{:04x})", + peer, + username, + msg.header.msg_type + ); } - } - } - if msg.header.msg_type == METHOD_ALLOCATE { - match alloc_clone - .allocate_for(peer, udp_clone.clone()) - .await - { - Ok(relay_addr) => { - let mut out = Vec::new(); - let success_type = - msg.header.msg_type | CLASS_SUCCESS; - out.extend_from_slice( - &success_type.to_be_bytes(), + AuthStatus::Challenge { nonce } => { + let resp = build_401_response( + &msg.header, + auth_clone.realm(), + &nonce, + 401, + "Unauthorized", ); - out.extend_from_slice(&0u16.to_be_bytes()); - out.extend_from_slice(&MAGIC_COOKIE_BYTES); - out.extend_from_slice( - &msg.header.transaction_id, - ); - let attr_val = encode_xor_relayed_address( - &relay_addr, - &msg.header.transaction_id, - ); - out.extend_from_slice( - &ATTR_XOR_RELAYED_ADDRESS.to_be_bytes(), - ); - out.extend_from_slice( - &((attr_val.len() as u16).to_be_bytes()), - ); - out.extend_from_slice(&attr_val); - while (out.len() % 4) != 0 { - out.extend_from_slice(&[0]); - } - let total_len = (out.len() - 20) as u16; - let len_bytes = total_len.to_be_bytes(); - out[2] = len_bytes[0]; - out[3] = len_bytes[1]; - if let Err(e) = tls_stream.write_all(&out).await + if let Err(e) = + tls_stream.write_all(&resp).await { tracing::error!( - "failed to write tls response: {:?}", + "failed to write tls challenge: {:?}", e ); } + continue; + } + AuthStatus::StaleNonce { nonce } => { + let resp = build_401_response( + &msg.header, + auth_clone.realm(), + &nonce, + 438, + "Stale Nonce", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls stale nonce: {:?}", + e + ); + } + continue; + } + AuthStatus::Reject { code, reason } => { + let resp = build_error_response( + &msg.header, + code, + reason, + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls auth error: {:?}", + e + ); + } + continue; } - Err(e) => tracing::error!( - "allocate failed (tls): {:?}", - e - ), } - continue; } - // default: send 401 challenge - let nonce = format!("nonce-{}", uuid::Uuid::new_v4()); - let resp = build_401_response( - &msg.header, - "niom-turn.local", - &nonce, - 401, - ); - if let Err(e) = tls_stream.write_all(&resp).await { - tracing::error!("failed to write tls 401: {:?}", e); + match msg.header.msg_type { + METHOD_ALLOCATE => { + use bytes::BytesMut; + match alloc_clone + .allocate_for(peer, udp_clone.clone()) + .await + { + Ok(relay_addr) => { + let mut out = BytesMut::new(); + let success_type = + msg.header.msg_type | CLASS_SUCCESS; + out.extend_from_slice( + &success_type.to_be_bytes(), + ); + out.extend_from_slice(&0u16.to_be_bytes()); + out.extend_from_slice(&MAGIC_COOKIE_BYTES); + + out.extend_from_slice( + &msg.header.transaction_id, + ); + let attr_val = encode_xor_relayed_address( + &relay_addr, + &msg.header.transaction_id, + ); + out.extend_from_slice( + &ATTR_XOR_RELAYED_ADDRESS.to_be_bytes(), + ); + out.extend_from_slice( + &((attr_val.len() as u16) + .to_be_bytes()), + ); + out.extend_from_slice(&attr_val); + while (out.len() % 4) != 0 { + out.extend_from_slice(&[0]); + } + let total_len = (out.len() - 20) as u16; + let len_bytes = total_len.to_be_bytes(); + out[2] = len_bytes[0]; + out[3] = len_bytes[1]; + if let Err(e) = + tls_stream.write_all(&out).await + { + tracing::error!( + "failed to write tls allocate success: {:?}", + e + ); + } + } + Err(e) => { + tracing::error!( + "allocate failed (tls): {:?}", + e + ); + let resp = build_error_response( + &msg.header, + 500, + "Allocate Failed", + ); + if let Err(e2) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls allocate error: {:?}", + e2 + ); + } + } + } + continue; + } + METHOD_CREATE_PERMISSION => { + if alloc_clone.get_allocation(&peer).is_none() { + tracing::warn!( + "create-permission without allocation from {} (tls)", + peer + ); + let resp = build_error_response( + &msg.header, + 437, + "Allocation Mismatch", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e + ); + } + continue; + } + + let mut added = 0usize; + for attr in msg + .attributes + .iter() + .filter(|a| a.typ == ATTR_XOR_PEER_ADDRESS) + { + if let Some(peer_addr) = decode_xor_peer_address( + &attr.value, + &msg.header.transaction_id, + ) { + match alloc_clone + .add_permission(peer, peer_addr) + { + Ok(()) => { + tracing::info!( + "added TLS permission for {} -> {}", + peer, + peer_addr + ); + added += 1; + } + Err(e) => { + tracing::error!( + "failed to persist TLS permission {} -> {}: {:?}", + peer, + peer_addr, + e + ); + } + } + } else { + tracing::warn!( + "invalid XOR-PEER-ADDRESS via TLS from {}", + peer + ); + } + } + + let resp = if added == 0 { + build_error_response( + &msg.header, + 400, + "No valid XOR-PEER-ADDRESS", + ) + } else { + build_success_response(&msg.header) + }; + if let Err(e) = tls_stream.write_all(&resp).await { + tracing::error!( + "failed to write tls response: {:?}", + e + ); + } + continue; + } + METHOD_CHANNEL_BIND => { + let allocation = match alloc_clone + .get_allocation(&peer) + { + Some(a) => a, + None => { + tracing::warn!( + "channel-bind without allocation from {} (tls)", + peer + ); + let resp = build_error_response( + &msg.header, + 437, + "Allocation Mismatch", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e + ); + } + continue; + } + }; + + let channel_attr = msg + .attributes + .iter() + .find(|a| a.typ == ATTR_CHANNEL_NUMBER); + let peer_attr = msg + .attributes + .iter() + .find(|a| a.typ == ATTR_XOR_PEER_ADDRESS); + + let channel = match channel_attr.and_then(|attr| { + if attr.value.len() >= 4 { + Some(u16::from_be_bytes([ + attr.value[0], + attr.value[1], + ])) + } else { + None + } + }) { + Some(c) => c, + None => { + let resp = build_error_response( + &msg.header, + 400, + "Missing CHANNEL-NUMBER", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e + ); + } + continue; + } + }; + + if channel < 0x4000 || channel > 0x7FFF { + let resp = build_error_response( + &msg.header, + 400, + "Channel Out Of Range", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e + ); + } + continue; + } + + let peer_addr = match peer_attr.and_then(|attr| { + decode_xor_peer_address( + &attr.value, + &msg.header.transaction_id, + ) + }) { + Some(addr) => addr, + None => { + let resp = build_error_response( + &msg.header, + 400, + "Missing XOR-PEER-ADDRESS", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e + ); + } + continue; + } + }; + + if !allocation.is_peer_allowed(&peer_addr) { + let resp = build_error_response( + &msg.header, + 403, + "Peer Not Permitted", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e + ); + } + continue; + } + + match alloc_clone + .add_channel_binding(peer, channel, peer_addr) + { + Ok(()) => { + tracing::info!( + "bound channel 0x{:04x} for {} -> {} over TLS", + channel, + peer, + peer_addr + ); + let resp = + build_success_response(&msg.header); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls response: {:?}", + e + ); + } + } + Err(e) => { + tracing::error!( + "failed TLS channel binding {} -> {} (0x{:04x}): {:?}", + peer, + peer_addr, + channel, + e + ); + let resp = build_error_response( + &msg.header, + 500, + "Channel Binding Failed", + ); + if let Err(e2) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e2 + ); + } + } + } + continue; + } + METHOD_SEND => { + let allocation = + match alloc_clone.get_allocation(&peer) { + Some(a) => a, + None => { + tracing::warn!( + "send without allocation from {} (tls)", + peer + ); + let resp = build_error_response( + &msg.header, + 437, + "Allocation Mismatch", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e + ); + } + continue; + } + }; + + let peer_attr = msg + .attributes + .iter() + .find(|a| a.typ == ATTR_XOR_PEER_ADDRESS); + let data_attr = msg + .attributes + .iter() + .find(|a| a.typ == ATTR_DATA); + + let peer_addr = match peer_attr.and_then(|attr| { + decode_xor_peer_address( + &attr.value, + &msg.header.transaction_id, + ) + }) { + Some(addr) => addr, + None => { + let resp = build_error_response( + &msg.header, + 400, + "Missing XOR-PEER-ADDRESS", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e + ); + } + continue; + } + }; + + let data_attr = match data_attr { + Some(attr) => attr, + None => { + let resp = build_error_response( + &msg.header, + 400, + "Missing DATA Attribute", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e + ); + } + continue; + } + }; + + if !allocation.is_peer_allowed(&peer_addr) { + let resp = build_error_response( + &msg.header, + 403, + "Peer Not Permitted", + ); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e + ); + } + continue; + } + + match allocation + .send_to_peer(peer_addr, &data_attr.value) + .await + { + Ok(sent) => { + tracing::info!( + "forwarded {} bytes from {} to {} via TLS session", + sent, + peer, + peer_addr + ); + let resp = + build_success_response(&msg.header); + if let Err(e) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls response: {:?}", + e + ); + } + } + Err(e) => { + tracing::error!( + "failed to send payload from {} to {} via TLS: {:?}", + peer, + peer_addr, + e + ); + let resp = build_error_response( + &msg.header, + 500, + "Peer Send Failed", + ); + if let Err(e2) = + tls_stream.write_all(&resp).await + { + tracing::error!( + "failed to write tls error: {:?}", + e2 + ); + } + } + } + continue; + } + METHOD_REFRESH => { + let resp = build_success_response(&msg.header); + if let Err(e) = tls_stream.write_all(&resp).await { + tracing::error!( + "failed to write tls refresh response: {:?}", + e + ); + } + continue; + } + METHOD_BINDING => { + let resp = build_success_response(&msg.header); + if let Err(e) = tls_stream.write_all(&resp).await { + tracing::error!( + "failed to write tls binding response: {:?}", + e + ); + } + continue; + } + _ => { + let nonce = auth_clone.mint_nonce(&peer); + let resp = build_401_response( + &msg.header, + auth_clone.realm(), + &nonce, + 401, + "Unauthorized", + ); + if let Err(e) = tls_stream.write_all(&resp).await { + tracing::error!( + "failed to write tls fallback challenge: {:?}", + e + ); + } + continue; + } } } else { tracing::debug!(