Introduced AuthManager with signed nonce handling and long-term credential validation.

This commit is contained in:
ghost 2025-11-16 18:51:13 +01:00
parent c77e95afdd
commit 7169ed0d1e
7 changed files with 1216 additions and 951 deletions

View File

@ -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. - Start with a minimal, well-tested parsing/utility layer and an in-memory credential store interface that can be replaced later.
Current status 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`. - STUN message parser + builder in `src/stun.rs`.
- CredentialStore trait + in-memory implementation in `src/auth.rs`. - Optional TLS listener (0.0.0.0:5349) mirrors the UDP path for `turns:` clients.
- Minimal logic: on any STUN request, server replies with a 401 challenge (REALM + NONCE).
Design Design
- Modules - Modules
- `stun.rs` - STUN/TURN message parsing and builders. - `stun.rs` STUN/TURN message parsing, MESSAGE-INTEGRITY helpers, and response builders.
- `auth.rs` - CredentialStore trait and an `InMemoryStore` impl. Use the trait to swap for DB-backed stores later. - `auth.rs` `AuthManager` orchestrates nonce minting, realm checking, and key derivation using the pluggable `CredentialStore` (default: `InMemoryStore`).
- `main.rs` - Bootstraps UDP listener, parses requests, and emits challenges for auth. - `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 Authentication & credential store
- `CredentialStore` is an async trait with `get_password(username) -> Option<String>`. - `CredentialStore` is an async trait with `get_password(username) -> Option<String>` used by `AuthManager`.
- The default `InMemoryStore` is provided for tests and local dev. Swap in a production store by implementing the trait. - `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 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. - Ensure UDP and TCP/TLS ports (3478/5349) are reachable from the internet when used as a public TURN server.
Auth caveat Auth caveat
- The current in-repo long-term auth implementation is intentionally minimal for the MVP and - The current implementation intentionally keeps things simple: credentials live in-memory, A1 keys
uses legacy constructs (A1/MD5 derivation + HMAC-SHA1 MESSAGE-INTEGRITY). MD5 is not recommended are derived via MD5 for RFC compatibility, and nonces are signed with HMAC-SHA1. Replace these
for new secure systems — this is present for RFC compatibility and testing only. We will replace pieces (Argon2-backed store, modern KDFs, nonce rotation) before production rollout. See
this with a secure credential workflow (ephemeral/REST credentials, PBKDF/KDF storage, or mTLS) `src/auth.rs` for the pluggable surface.
before any production deployment. See `src/auth.rs` for the current simple store and helpers.
Milestone 1 — Protocol Backlog Milestone 1 — Protocol Backlog
------------------------------ ------------------------------
@ -154,11 +155,16 @@ Das Projekt kann eine JSON-Konfigdatei `appsettings.json` im Arbeitsverzeichnis
"username": "testuser", "username": "testuser",
"password": "secretpassword" "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 Deployment & TLS / Long-term Auth roadmap
----------------------------------------- -----------------------------------------

View File

@ -9,5 +9,10 @@
"username": "testuser", "username": "testuser",
"password": "secretpassword" "password": "secretpassword"
} }
] ],
"auth": {
"realm": "niom-turn.local",
"nonce_secret": null,
"nonce_ttl_seconds": 300
}
} }

View File

@ -1,8 +1,16 @@
//! Authentication helpers and the in-memory credential store used for the MVP server. //! Authentication helpers and the in-memory credential store used for the MVP server.
//! Backlog: Argon2-backed storage, nonce lifecycle, and integration with persistent secrets. //! 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 crate::traits::CredentialStore;
use async_trait::async_trait; use async_trait::async_trait;
use hmac::{Hmac, Mac};
use sha1::Sha1;
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
/// Simple in-memory credential store for MVP /// Simple in-memory credential store for MVP
#[derive(Clone, Default)] #[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<u8>,
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<S: CredentialStore + Clone> {
store: S,
settings: AuthSettings,
}
impl<S: CredentialStore + Clone> Clone for AuthManager<S> {
fn clone(&self) -> Self {
Self {
store: self.store.clone(),
settings: self.settings.clone(),
}
}
}
impl<S: CredentialStore + Clone> AuthManager<S> {
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<String> {
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<u8> {
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::<u64>() {
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<Sha1>;
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) /// Helper: compute MESSAGE-INTEGRITY (HMAC-SHA1 as bytes)
pub fn compute_hmac_sha1_bytes(key: &str, data: &[u8]) -> Vec<u8> { pub fn compute_hmac_sha1_bytes(key: &str, data: &[u8]) -> Vec<u8> {
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};

View File

@ -3,12 +3,43 @@
use serde::Deserialize; use serde::Deserialize;
use std::path::Path; use std::path::Path;
fn default_realm() -> String {
"niom-turn.local".to_string()
}
fn default_nonce_ttl_seconds() -> u64 {
300
}
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct CredentialEntry { pub struct CredentialEntry {
pub username: String, pub username: String,
pub password: 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<String>,
/// 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)] #[derive(Debug, Deserialize, Clone)]
pub struct ServerOptions { pub struct ServerOptions {
/// Listen address, e.g. "0.0.0.0:3478" /// Listen address, e.g. "0.0.0.0:3478"
@ -25,6 +56,9 @@ pub struct Config {
/// Initial credentials to populate the credential store /// Initial credentials to populate the credential store
#[serde(default)] #[serde(default)]
pub credentials: Vec<CredentialEntry>, pub credentials: Vec<CredentialEntry>,
/// Authentication behaviour advertised to clients.
#[serde(default)]
pub auth: AuthOptions,
} }
impl Config { impl Config {

View File

@ -7,14 +7,13 @@ use tracing::{error, info};
// Use the library crate's public modules instead of local `mod` declarations. // Use the library crate's public modules instead of local `mod` declarations.
use niom_turn::alloc::AllocationManager; use niom_turn::alloc::AllocationManager;
use niom_turn::auth::InMemoryStore; use niom_turn::auth::{AuthManager, AuthStatus, InMemoryStore};
use niom_turn::config::Config; use niom_turn::config::{AuthOptions, Config};
use niom_turn::constants::*; use niom_turn::constants::*;
use niom_turn::stun::{ use niom_turn::stun::{
build_401_response, build_error_response, build_success_response, decode_xor_peer_address, 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] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
@ -44,6 +43,7 @@ async fn main() -> anyhow::Result<()> {
username: "testuser".into(), username: "testuser".into(),
password: "secretpassword".into(), password: "secretpassword".into(),
}], }],
auth: AuthOptions::default(),
} }
} }
}; };
@ -56,6 +56,8 @@ async fn main() -> anyhow::Result<()> {
creds.insert(&c.username, &c.password); 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. // Bind the UDP socket that receives STUN/TURN traffic from WebRTC clients.
let udp = UdpSocket::bind(bind_addr).await?; let udp = UdpSocket::bind(bind_addr).await?;
let udp = Arc::new(udp); let udp = Arc::new(udp);
@ -65,10 +67,10 @@ async fn main() -> anyhow::Result<()> {
// Spawn the asynchronous packet loop that handles all UDP requests. // Spawn the asynchronous packet loop that handles all UDP requests.
let udp_clone = udp.clone(); let udp_clone = udp.clone();
let creds_clone = creds.clone(); let auth_clone = auth.clone();
let alloc_clone = alloc_mgr.clone(); let alloc_clone = alloc_mgr.clone();
tokio::spawn(async move { 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); 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. // 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()) { if let (Some(cert), Some(key)) = (cfg.server.tls_cert.clone(), cfg.server.tls_key.clone()) {
let udp_for_tls = udp.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(); let alloc_for_tls = alloc_mgr.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = niom_turn::tls::serve_tls( if let Err(e) = niom_turn::tls::serve_tls(
@ -84,7 +86,7 @@ async fn main() -> anyhow::Result<()> {
&cert, &cert,
&key, &key,
udp_for_tls, udp_for_tls,
creds_for_tls, auth_for_tls,
alloc_for_tls, alloc_for_tls,
) )
.await .await
@ -102,7 +104,7 @@ async fn main() -> anyhow::Result<()> {
async fn udp_reader_loop( async fn udp_reader_loop(
udp: Arc<UdpSocket>, udp: Arc<UdpSocket>,
creds: InMemoryStore, auth: AuthManager<InMemoryStore>,
allocs: AllocationManager, allocs: AllocationManager,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut buf = vec![0u8; 1500]; let mut buf = vec![0u8; 1500];
@ -119,41 +121,71 @@ async fn udp_reader_loop(
msg.header.msg_type, msg.header.msg_type,
len len
); );
// Fast-path authenticated requests when MESSAGE-INTEGRITY can be validated. let requires_auth = matches!(
if let Some(_mi_attr) = find_message_integrity(&msg) { msg.header.msg_type,
// For MVP we expect username attribute (USERNAME) to be present METHOD_ALLOCATE
let username_attr = msg.attributes.iter().find(|a| a.typ == ATTR_USERNAME); | METHOD_CREATE_PERMISSION
if let Some(u) = username_attr { | METHOD_CHANNEL_BIND
if let Ok(username) = std::str::from_utf8(&u.value) { | METHOD_SEND
// lookup password | METHOD_REFRESH
let store = creds.clone(); );
let pw = store.get_password(username).await;
if let Some(password) = pw { if requires_auth {
let valid = validate_message_integrity(&msg, &password); match auth.authenticate(&msg, &peer).await {
if valid { AuthStatus::Granted { username } => {
tracing::info!("MI valid for user {}", username); tracing::debug!(
// Handle authenticated Allocate to mint a relay binding for the client. "TURN auth ok for {} as {} (0x{:04x})",
if msg.header.msg_type == METHOD_ALLOCATE { 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;
}
}
match msg.header.msg_type {
METHOD_ALLOCATE => {
use bytes::BytesMut;
match allocs.allocate_for(peer, udp.clone()).await { match allocs.allocate_for(peer, udp.clone()).await {
Ok(relay_addr) => { Ok(relay_addr) => {
use bytes::BytesMut;
let mut out = BytesMut::new(); let mut out = BytesMut::new();
let success_type = msg.header.msg_type | CLASS_SUCCESS; let success_type = msg.header.msg_type | CLASS_SUCCESS;
out.extend_from_slice(&success_type.to_be_bytes()); out.extend_from_slice(&success_type.to_be_bytes());
out.extend_from_slice(&0u16.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(&MAGIC_COOKIE_U32.to_be_bytes());
out.extend_from_slice(&msg.header.transaction_id); out.extend_from_slice(&msg.header.transaction_id);
// RFC: XOR-RELAYED-ADDRESS (0x0016)
let attr_val = encode_xor_relayed_address( let attr_val = encode_xor_relayed_address(
&relay_addr, &relay_addr,
&msg.header.transaction_id, &msg.header.transaction_id,
); );
out.extend_from_slice( out.extend_from_slice(&ATTR_XOR_RELAYED_ADDRESS.to_be_bytes());
&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.len() as u16).to_be_bytes()),
);
out.extend_from_slice(&attr_val); out.extend_from_slice(&attr_val);
while (out.len() % 4) != 0 { while (out.len() % 4) != 0 {
out.extend_from_slice(&[0]); out.extend_from_slice(&[0]);
@ -163,27 +195,27 @@ async fn udp_reader_loop(
out[2] = len_bytes[0]; out[2] = len_bytes[0];
out[3] = len_bytes[1]; out[3] = len_bytes[1];
let vec_out = out.to_vec(); let vec_out = out.to_vec();
tracing::info!("sending allocate success (mi-valid) -> {} bytes hex={} ", vec_out.len(), hex::encode(&vec_out)); tracing::info!(
"sending allocate success -> {} bytes hex={} ",
vec_out.len(),
hex::encode(&vec_out)
);
let _ = udp.send_to(&vec_out, &peer).await; 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; continue;
} }
Err(e) => tracing::error!( METHOD_CREATE_PERMISSION => {
"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() { if allocs.get_allocation(&peer).is_none() {
tracing::warn!( tracing::warn!("create-permission without allocation from {}", peer);
"create-permission without allocation from {}", let resp =
peer build_error_response(&msg.header, 437, "Allocation Mismatch");
);
let resp = build_error_response(
&msg.header,
437,
"Allocation Mismatch",
);
let _ = udp.send_to(&resp, &peer).await; let _ = udp.send_to(&resp, &peer).await;
continue; continue;
} }
@ -194,10 +226,9 @@ async fn udp_reader_loop(
.iter() .iter()
.filter(|a| a.typ == ATTR_XOR_PEER_ADDRESS) .filter(|a| a.typ == ATTR_XOR_PEER_ADDRESS)
{ {
if let Some(peer_addr) = decode_xor_peer_address( if let Some(peer_addr) =
&attr.value, decode_xor_peer_address(&attr.value, &msg.header.transaction_id)
&msg.header.transaction_id, {
) {
match allocs.add_permission(peer, peer_addr) { match allocs.add_permission(peer, peer_addr) {
Ok(()) => { Ok(()) => {
tracing::info!( tracing::info!(
@ -208,51 +239,43 @@ async fn udp_reader_loop(
added += 1; added += 1;
} }
Err(e) => { Err(e) => {
tracing::error!("failed to persist permission {} -> {}: {:?}", peer, peer_addr, e); tracing::error!(
"failed to persist permission {} -> {}: {:?}",
peer,
peer_addr,
e
);
} }
} }
} else { } else {
tracing::warn!( tracing::warn!("invalid XOR-PEER-ADDRESS in request from {}", peer);
"invalid XOR-PEER-ADDRESS in request from {}",
peer
);
} }
} }
if added == 0 { if added == 0 {
let resp = build_error_response( let resp =
&msg.header, build_error_response(&msg.header, 400, "No valid XOR-PEER-ADDRESS");
400,
"No valid XOR-PEER-ADDRESS",
);
let _ = udp.send_to(&resp, &peer).await; let _ = udp.send_to(&resp, &peer).await;
} else { } else {
let resp = build_success_response(&msg.header); let resp = build_success_response(&msg.header);
let _ = udp.send_to(&resp, &peer).await; let _ = udp.send_to(&resp, &peer).await;
} }
continue; continue;
} else if msg.header.msg_type == METHOD_CHANNEL_BIND { }
METHOD_CHANNEL_BIND => {
let allocation = match allocs.get_allocation(&peer) { let allocation = match allocs.get_allocation(&peer) {
Some(a) => a, Some(a) => a,
None => { None => {
tracing::warn!( tracing::warn!("channel-bind without allocation from {}", peer);
"channel-bind without allocation from {}", let resp =
peer build_error_response(&msg.header, 437, "Allocation Mismatch");
);
let resp = build_error_response(
&msg.header,
437,
"Allocation Mismatch",
);
let _ = udp.send_to(&resp, &peer).await; let _ = udp.send_to(&resp, &peer).await;
continue; continue;
} }
}; };
let channel_attr = msg let channel_attr =
.attributes msg.attributes.iter().find(|a| a.typ == ATTR_CHANNEL_NUMBER);
.iter()
.find(|a| a.typ == ATTR_CHANNEL_NUMBER);
let peer_attr = msg let peer_attr = msg
.attributes .attributes
.iter() .iter()
@ -278,20 +301,14 @@ async fn udp_reader_loop(
}; };
if channel < 0x4000 || channel > 0x7FFF { if channel < 0x4000 || channel > 0x7FFF {
let resp = build_error_response( let resp =
&msg.header, build_error_response(&msg.header, 400, "Channel Out Of Range");
400,
"Channel Out Of Range",
);
let _ = udp.send_to(&resp, &peer).await; let _ = udp.send_to(&resp, &peer).await;
continue; continue;
} }
let peer_addr = match peer_attr.and_then(|attr| { let peer_addr = match peer_attr.and_then(|attr| {
decode_xor_peer_address( decode_xor_peer_address(&attr.value, &msg.header.transaction_id)
&attr.value,
&msg.header.transaction_id,
)
}) { }) {
Some(addr) => addr, Some(addr) => addr,
None => { None => {
@ -306,11 +323,7 @@ async fn udp_reader_loop(
}; };
if !allocation.is_peer_allowed(&peer_addr) { if !allocation.is_peer_allowed(&peer_addr) {
let resp = build_error_response( let resp = build_error_response(&msg.header, 403, "Peer Not Permitted");
&msg.header,
403,
"Peer Not Permitted",
);
let _ = udp.send_to(&resp, &peer).await; let _ = udp.send_to(&resp, &peer).await;
continue; continue;
} }
@ -329,7 +342,10 @@ async fn udp_reader_loop(
Err(e) => { Err(e) => {
tracing::error!( tracing::error!(
"failed to add channel binding {} -> {} (channel 0x{:04x}): {:?}", "failed to add channel binding {} -> {} (channel 0x{:04x}): {:?}",
peer, peer_addr, channel, e peer,
peer_addr,
channel,
e
); );
let resp = build_error_response( let resp = build_error_response(
&msg.header, &msg.header,
@ -340,16 +356,14 @@ async fn udp_reader_loop(
} }
} }
continue; continue;
} else if msg.header.msg_type == METHOD_SEND { }
METHOD_SEND => {
let allocation = match allocs.get_allocation(&peer) { let allocation = match allocs.get_allocation(&peer) {
Some(a) => a, Some(a) => a,
None => { None => {
tracing::warn!("send without allocation from {}", peer); tracing::warn!("send without allocation from {}", peer);
let resp = build_error_response( let resp =
&msg.header, build_error_response(&msg.header, 437, "Allocation Mismatch");
437,
"Allocation Mismatch",
);
let _ = udp.send_to(&resp, &peer).await; let _ = udp.send_to(&resp, &peer).await;
continue; continue;
} }
@ -359,14 +373,10 @@ async fn udp_reader_loop(
.attributes .attributes
.iter() .iter()
.find(|a| a.typ == ATTR_XOR_PEER_ADDRESS); .find(|a| a.typ == ATTR_XOR_PEER_ADDRESS);
let data_attr = let data_attr = msg.attributes.iter().find(|a| a.typ == ATTR_DATA);
msg.attributes.iter().find(|a| a.typ == ATTR_DATA);
let peer_addr = match peer_attr.and_then(|attr| { let peer_addr = match peer_attr.and_then(|attr| {
decode_xor_peer_address( decode_xor_peer_address(&attr.value, &msg.header.transaction_id)
&attr.value,
&msg.header.transaction_id,
)
}) { }) {
Some(addr) => addr, Some(addr) => addr,
None => { None => {
@ -394,17 +404,12 @@ async fn udp_reader_loop(
}; };
if !allocation.is_peer_allowed(&peer_addr) { if !allocation.is_peer_allowed(&peer_addr) {
let resp = build_error_response( let resp = build_error_response(&msg.header, 403, "Peer Not Permitted");
&msg.header,
403,
"Peer Not Permitted",
);
let _ = udp.send_to(&resp, &peer).await; let _ = udp.send_to(&resp, &peer).await;
continue; continue;
} }
match allocation.send_to_peer(peer_addr, &data_attr.value).await match allocation.send_to_peer(peer_addr, &data_attr.value).await {
{
Ok(sent) => { Ok(sent) => {
tracing::info!( tracing::info!(
"forwarded {} bytes from {} to peer {}", "forwarded {} bytes from {} to peer {}",
@ -422,75 +427,41 @@ async fn udp_reader_loop(
peer_addr, peer_addr,
e e
); );
let resp = build_error_response( let resp =
&msg.header, build_error_response(&msg.header, 500, "Peer Send Failed");
500,
"Peer Send Failed",
);
let _ = udp.send_to(&resp, &peer).await; let _ = udp.send_to(&resp, &peer).await;
} }
} }
continue; continue;
} }
// Non-specific success path: echo a success response so the client continues handshake. METHOD_REFRESH => {
// Refresh support is still MVP-level; acknowledge so clients extend allocations.
let resp = build_success_response(&msg.header); let resp = build_success_response(&msg.header);
let _ = udp.send_to(&resp, &peer).await; let _ = udp.send_to(&resp, &peer).await;
continue; continue;
} else {
tracing::info!("MI invalid for user {}", username);
}
} else {
tracing::info!("unknown user {}", username);
}
}
}
}
// 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);
}
} }
_ => {
let resp = build_error_response(&msg.header, 420, "Unknown TURN Method");
let _ = udp.send_to(&resp, &peer).await;
continue; continue;
} }
}
}
// Everything else receives a 401 challenge so the client can retry with credentials. match msg.header.msg_type {
let nonce = format!("nonce-{}", uuid::Uuid::new_v4()); METHOD_BINDING => {
let resp = build_401_response(&msg.header, "niom-turn.local", &nonce, 401); 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 { if let Err(e) = udp.send_to(&resp, &peer).await {
error!("failed to send 401: {:?}", e); error!("failed to send 401: {:?}", e);
} }
}
}
} else { } else {
tracing::debug!("Non-STUN or parse error from {} len={}", peer, len); tracing::debug!("Non-STUN or parse error from {} len={}", peer, len);
} }

View File

@ -66,8 +66,14 @@ pub fn parse_message(buf: &[u8]) -> Result<StunMessage, ParseError> {
}) })
} }
/// Build a minimal 401 error response (REALM + NONCE). Returns the bytes to send. /// 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) -> Vec<u8> { pub fn build_401_response(
req: &StunHeader,
realm: &str,
nonce: &str,
err_code: u16,
reason: &str,
) -> Vec<u8> {
use bytes::BytesMut; use bytes::BytesMut;
let mut buf = BytesMut::new(); let mut buf = BytesMut::new();
// Error response type for TURN: reuse the request method with error class bits set // 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(&MAGIC_COOKIE_BYTES);
buf.extend_from_slice(&req.transaction_id); 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) // REALM (RFC attr)
let realm_bytes = realm.as_bytes(); let realm_bytes = realm.as_bytes();
buf.extend_from_slice(&ATTR_REALM.to_be_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. /// 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 /// 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). /// 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) { if let Some(mi) = find_message_integrity(msg) {
// MESSAGE-INTEGRITY attribute value is 20 bytes (HMAC-SHA1) // MESSAGE-INTEGRITY attribute value is 20 bytes (HMAC-SHA1)
if mi.value.len() != 20 { if mi.value.len() != 20 {
@ -189,11 +209,11 @@ pub fn compute_fingerprint(msg: &[u8]) -> u32 {
} }
/// Compute MESSAGE-INTEGRITY (HMAC-SHA1) over the message /// Compute MESSAGE-INTEGRITY (HMAC-SHA1) over the message
pub fn compute_message_integrity(key: &str, msg: &[u8]) -> Vec<u8> { pub fn compute_message_integrity(key: &[u8], msg: &[u8]) -> Vec<u8> {
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use sha1::Sha1; use sha1::Sha1;
type HmacSha1 = Hmac<Sha1>; type HmacSha1 = Hmac<Sha1>;
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.update(msg);
mac.finalize().into_bytes().to_vec() mac.finalize().into_bytes().to_vec()
} }
@ -284,7 +304,7 @@ mod tests {
cookie: MAGIC_COOKIE_U32, cookie: MAGIC_COOKIE_U32,
transaction_id: [2u8; 12], 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 // parse back should succeed
let parsed = parse_message(&out).expect("parse resp"); let parsed = parse_message(&out).expect("parse resp");
assert!(!parsed.attributes.is_empty()); assert!(!parsed.attributes.is_empty());
@ -331,7 +351,7 @@ mod tests {
buf[3] = len_bytes[1]; buf[3] = len_bytes[1];
// Compute HMAC over message up to MI attribute header (mi_attr_offset) // 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 // place first 20 bytes into mi value
for i in 0..20 { for i in 0..20 {
buf[mi_val_pos + i] = hmac[i]; buf[mi_val_pos + i] = hmac[i];
@ -339,12 +359,12 @@ mod tests {
// Parse and validate // Parse and validate
let parsed = parse_message(&buf).expect("parsed"); 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 // tamper: change one byte -> invalid
let mut tampered = buf.to_vec(); let mut tampered = buf.to_vec();
tampered[10] ^= 0xFF; tampered[10] ^= 0xFF;
let parsed2 = parse_message(&tampered).expect("parsed2"); let parsed2 = parse_message(&tampered).expect("parsed2");
assert!(!validate_message_integrity(&parsed2, password)); assert!(!validate_message_integrity(&parsed2, password.as_bytes()));
} }
} }

View File

@ -10,13 +10,12 @@ use tokio_rustls::rustls::{Certificate, PrivateKey, ServerConfig};
use tokio_rustls::TlsAcceptor; use tokio_rustls::TlsAcceptor;
use crate::alloc::AllocationManager; use crate::alloc::AllocationManager;
use crate::auth::InMemoryStore; use crate::auth::{AuthManager, AuthStatus, InMemoryStore};
use crate::constants::*; use crate::constants::*;
use crate::stun::{ use crate::stun::{
build_401_response, build_error_response, build_success_response, decode_xor_peer_address, 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<Vec<Certificate>> { fn load_certs(path: &str) -> anyhow::Result<Vec<Certificate>> {
let f = File::open(path).context("opening cert file")?; let f = File::open(path).context("opening cert file")?;
@ -49,7 +48,7 @@ pub async fn serve_tls(
cert_path: &str, cert_path: &str,
key_path: &str, key_path: &str,
udp_sock: std::sync::Arc<tokio::net::UdpSocket>, udp_sock: std::sync::Arc<tokio::net::UdpSocket>,
creds: InMemoryStore, auth: AuthManager<InMemoryStore>,
allocs: AllocationManager, allocs: AllocationManager,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let certs = load_certs(cert_path)?; let certs = load_certs(cert_path)?;
@ -60,7 +59,6 @@ pub async fn serve_tls(
.with_no_client_auth() .with_no_client_auth()
.with_single_cert(certs, key)?; .with_single_cert(certs, key)?;
// set protocols etc if needed
let acceptor = TlsAcceptor::from(Arc::new(cfg)); let acceptor = TlsAcceptor::from(Arc::new(cfg));
let listener = TcpListener::bind(bind).await?; let listener = TcpListener::bind(bind).await?;
@ -70,7 +68,7 @@ pub async fn serve_tls(
let (stream, peer) = listener.accept().await?; let (stream, peer) = listener.accept().await?;
let acceptor = acceptor.clone(); let acceptor = acceptor.clone();
let udp_clone = udp_sock.clone(); let udp_clone = udp_sock.clone();
let creds_clone = creds.clone(); let auth_clone = auth.clone();
let alloc_clone = allocs.clone(); let alloc_clone = allocs.clone();
tokio::spawn(async move { tokio::spawn(async move {
@ -79,6 +77,7 @@ pub async fn serve_tls(
tracing::info!("accepted TLS connection from {}", peer); tracing::info!("accepted TLS connection from {}", peer);
let mut read_buf = vec![0u8; 4096]; let mut read_buf = vec![0u8; 4096];
let mut buffer: Vec<u8> = Vec::new(); let mut buffer: Vec<u8> = Vec::new();
loop { loop {
match tls_stream.read(&mut read_buf).await { match tls_stream.read(&mut read_buf).await {
Ok(0) => { Ok(0) => {
@ -87,7 +86,6 @@ pub async fn serve_tls(
} }
Ok(n) => { Ok(n) => {
buffer.extend_from_slice(&read_buf[..n]); buffer.extend_from_slice(&read_buf[..n]);
// Try to extract STUN messages framed by header length
while buffer.len() >= 20 { while buffer.len() >= 20 {
let len = u16::from_be_bytes([buffer[2], buffer[3]]) as usize; let len = u16::from_be_bytes([buffer[2], buffer[3]]) as usize;
let total = len + 20; let total = len + 20;
@ -96,70 +94,175 @@ pub async fn serve_tls(
} }
let chunk = buffer.drain(..total).collect::<Vec<u8>>(); let chunk = buffer.drain(..total).collect::<Vec<u8>>();
if let Ok(msg) = parse_message(&chunk) { 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!( tracing::info!(
"MI valid for user {} on TLS", "STUN/TURN over TLS from {} type=0x{:04x} len={}",
username peer,
msg.header.msg_type,
total
); );
if msg.header.msg_type
== METHOD_ALLOCATE let requires_auth = matches!(
msg.header.msg_type,
METHOD_ALLOCATE
| METHOD_CREATE_PERMISSION
| METHOD_CHANNEL_BIND
| METHOD_SEND
| METHOD_REFRESH
);
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
);
}
AuthStatus::Challenge { nonce } => {
let resp = build_401_response(
&msg.header,
auth_clone.realm(),
&nonce,
401,
"Unauthorized",
);
if let Err(e) =
tls_stream.write_all(&resp).await
{ {
match alloc_clone.allocate_for(peer, udp_clone.clone()).await { tracing::error!(
Ok(relay_addr) => { "failed to write tls challenge: {:?}",
let mut out = Vec::new(); e
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; continue;
} }
Err(e) => tracing::error!("allocate failed after MI valid (tls): {:?}", e), 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
);
} }
} else if msg.header.msg_type continue;
== METHOD_CREATE_PERMISSION }
AuthStatus::Reject { code, reason } => {
let resp = build_error_response(
&msg.header,
code,
reason,
);
if let Err(e) =
tls_stream.write_all(&resp).await
{ {
if alloc_clone tracing::error!(
.get_allocation(&peer) "failed to write tls auth error: {:?}",
.is_none() e
);
}
continue;
}
}
}
match msg.header.msg_type {
METHOD_ALLOCATE => {
use bytes::BytesMut;
match alloc_clone
.allocate_for(peer, udp_clone.clone())
.await
{ {
tracing::warn!("create-permission without allocation from {} (tls)", peer); 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( let resp = build_error_response(
&msg.header, &msg.header,
437, 437,
"Allocation Mismatch", "Allocation Mismatch",
); );
if let Err(e) = tls_stream if let Err(e) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!("failed to write tls error: {:?}", e); tracing::error!(
"failed to write tls error: {:?}",
e
);
} }
continue; continue;
} }
@ -168,27 +271,37 @@ pub async fn serve_tls(
for attr in msg for attr in msg
.attributes .attributes
.iter() .iter()
.filter(|a| { .filter(|a| a.typ == ATTR_XOR_PEER_ADDRESS)
a.typ
== ATTR_XOR_PEER_ADDRESS
})
{ {
if let Some(peer_addr) = if let Some(peer_addr) = decode_xor_peer_address(
decode_xor_peer_address(
&attr.value, &attr.value,
&msg.header &msg.header.transaction_id,
.transaction_id, ) {
) match alloc_clone
.add_permission(peer, peer_addr)
{ {
match alloc_clone.add_permission(peer, peer_addr) {
Ok(()) => { Ok(()) => {
tracing::info!("added TLS permission for {} -> {}", peer, peer_addr); tracing::info!(
"added TLS permission for {} -> {}",
peer,
peer_addr
);
added += 1; added += 1;
} }
Err(e) => tracing::error!("failed to persist TLS permission {} -> {}: {:?}", peer, peer_addr, e), Err(e) => {
tracing::error!(
"failed to persist TLS permission {} -> {}: {:?}",
peer,
peer_addr,
e
);
}
} }
} else { } else {
tracing::warn!("invalid XOR-PEER-ADDRESS via TLS from {}", peer); tracing::warn!(
"invalid XOR-PEER-ADDRESS via TLS from {}",
peer
);
} }
} }
@ -199,20 +312,17 @@ pub async fn serve_tls(
"No valid XOR-PEER-ADDRESS", "No valid XOR-PEER-ADDRESS",
) )
} else { } else {
build_success_response( build_success_response(&msg.header)
&msg.header,
)
}; };
if let Err(e) = tls_stream if let Err(e) = tls_stream.write_all(&resp).await {
.write_all(&resp) tracing::error!(
.await "failed to write tls response: {:?}",
{ e
tracing::error!("failed to write tls response: {:?}", e); );
} }
continue; continue;
} else if msg.header.msg_type }
== METHOD_CHANNEL_BIND METHOD_CHANNEL_BIND => {
{
let allocation = match alloc_clone let allocation = match alloc_clone
.get_allocation(&peer) .get_allocation(&peer)
{ {
@ -227,9 +337,8 @@ pub async fn serve_tls(
437, 437,
"Allocation Mismatch", "Allocation Mismatch",
); );
if let Err(e) = tls_stream if let Err(e) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -243,30 +352,18 @@ pub async fn serve_tls(
let channel_attr = msg let channel_attr = msg
.attributes .attributes
.iter() .iter()
.find(|a| { .find(|a| a.typ == ATTR_CHANNEL_NUMBER);
a.typ == ATTR_CHANNEL_NUMBER
});
let peer_attr = msg let peer_attr = msg
.attributes .attributes
.iter() .iter()
.find(|a| { .find(|a| a.typ == ATTR_XOR_PEER_ADDRESS);
a.typ
== ATTR_XOR_PEER_ADDRESS
});
let channel = match channel_attr let channel = match channel_attr.and_then(|attr| {
.and_then(|attr| {
if attr.value.len() >= 4 { if attr.value.len() >= 4 {
Some( Some(u16::from_be_bytes([
u16::from_be_bytes( attr.value[0],
[ attr.value[1],
attr.value ]))
[0],
attr.value
[1],
],
),
)
} else { } else {
None None
} }
@ -278,9 +375,8 @@ pub async fn serve_tls(
400, 400,
"Missing CHANNEL-NUMBER", "Missing CHANNEL-NUMBER",
); );
if let Err(e) = tls_stream if let Err(e) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -291,17 +387,14 @@ pub async fn serve_tls(
} }
}; };
if channel < 0x4000 if channel < 0x4000 || channel > 0x7FFF {
|| channel > 0x7FFF
{
let resp = build_error_response( let resp = build_error_response(
&msg.header, &msg.header,
400, 400,
"Channel Out Of Range", "Channel Out Of Range",
); );
if let Err(e) = tls_stream if let Err(e) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -311,12 +404,10 @@ pub async fn serve_tls(
continue; continue;
} }
let peer_addr = match peer_attr let peer_addr = match peer_attr.and_then(|attr| {
.and_then(|attr| {
decode_xor_peer_address( decode_xor_peer_address(
&attr.value, &attr.value,
&msg.header &msg.header.transaction_id,
.transaction_id,
) )
}) { }) {
Some(addr) => addr, Some(addr) => addr,
@ -326,9 +417,8 @@ pub async fn serve_tls(
400, 400,
"Missing XOR-PEER-ADDRESS", "Missing XOR-PEER-ADDRESS",
); );
if let Err(e) = tls_stream if let Err(e) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -339,17 +429,14 @@ pub async fn serve_tls(
} }
}; };
if !allocation if !allocation.is_peer_allowed(&peer_addr) {
.is_peer_allowed(&peer_addr)
{
let resp = build_error_response( let resp = build_error_response(
&msg.header, &msg.header,
403, 403,
"Peer Not Permitted", "Peer Not Permitted",
); );
if let Err(e) = tls_stream if let Err(e) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -360,9 +447,8 @@ pub async fn serve_tls(
} }
match alloc_clone match alloc_clone
.add_channel_binding( .add_channel_binding(peer, channel, peer_addr)
peer, channel, peer_addr, {
) {
Ok(()) => { Ok(()) => {
tracing::info!( tracing::info!(
"bound channel 0x{:04x} for {} -> {} over TLS", "bound channel 0x{:04x} for {} -> {} over TLS",
@ -371,12 +457,9 @@ pub async fn serve_tls(
peer_addr peer_addr
); );
let resp = let resp =
build_success_response( build_success_response(&msg.header);
&msg.header, if let Err(e) =
); tls_stream.write_all(&resp).await
if let Err(e) = tls_stream
.write_all(&resp)
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls response: {:?}", "failed to write tls response: {:?}",
@ -397,9 +480,8 @@ pub async fn serve_tls(
500, 500,
"Channel Binding Failed", "Channel Binding Failed",
); );
if let Err(e2) = tls_stream if let Err(e2) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -409,12 +491,10 @@ pub async fn serve_tls(
} }
} }
continue; continue;
} else if msg.header.msg_type }
== METHOD_SEND METHOD_SEND => {
{ let allocation =
let allocation = match alloc_clone match alloc_clone.get_allocation(&peer) {
.get_allocation(&peer)
{
Some(a) => a, Some(a) => a,
None => { None => {
tracing::warn!( tracing::warn!(
@ -426,9 +506,8 @@ pub async fn serve_tls(
437, 437,
"Allocation Mismatch", "Allocation Mismatch",
); );
if let Err(e) = tls_stream if let Err(e) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -442,21 +521,16 @@ pub async fn serve_tls(
let peer_attr = msg let peer_attr = msg
.attributes .attributes
.iter() .iter()
.find(|a| { .find(|a| a.typ == ATTR_XOR_PEER_ADDRESS);
a.typ
== ATTR_XOR_PEER_ADDRESS
});
let data_attr = msg let data_attr = msg
.attributes .attributes
.iter() .iter()
.find(|a| a.typ == ATTR_DATA); .find(|a| a.typ == ATTR_DATA);
let peer_addr = match peer_attr let peer_addr = match peer_attr.and_then(|attr| {
.and_then(|attr| {
decode_xor_peer_address( decode_xor_peer_address(
&attr.value, &attr.value,
&msg.header &msg.header.transaction_id,
.transaction_id,
) )
}) { }) {
Some(addr) => addr, Some(addr) => addr,
@ -466,9 +540,8 @@ pub async fn serve_tls(
400, 400,
"Missing XOR-PEER-ADDRESS", "Missing XOR-PEER-ADDRESS",
); );
if let Err(e) = tls_stream if let Err(e) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -487,9 +560,8 @@ pub async fn serve_tls(
400, 400,
"Missing DATA Attribute", "Missing DATA Attribute",
); );
if let Err(e) = tls_stream if let Err(e) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -500,17 +572,14 @@ pub async fn serve_tls(
} }
}; };
if !allocation if !allocation.is_peer_allowed(&peer_addr) {
.is_peer_allowed(&peer_addr)
{
let resp = build_error_response( let resp = build_error_response(
&msg.header, &msg.header,
403, 403,
"Peer Not Permitted", "Peer Not Permitted",
); );
if let Err(e) = tls_stream if let Err(e) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -521,10 +590,7 @@ pub async fn serve_tls(
} }
match allocation match allocation
.send_to_peer( .send_to_peer(peer_addr, &data_attr.value)
peer_addr,
&data_attr.value,
)
.await .await
{ {
Ok(sent) => { Ok(sent) => {
@ -535,12 +601,9 @@ pub async fn serve_tls(
peer_addr peer_addr
); );
let resp = let resp =
build_success_response( build_success_response(&msg.header);
&msg.header, if let Err(e) =
); tls_stream.write_all(&resp).await
if let Err(e) = tls_stream
.write_all(&resp)
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls response: {:?}", "failed to write tls response: {:?}",
@ -555,15 +618,13 @@ pub async fn serve_tls(
peer_addr, peer_addr,
e e
); );
let resp = let resp = build_error_response(
build_error_response(
&msg.header, &msg.header,
500, 500,
"Peer Send Failed", "Peer Send Failed",
); );
if let Err(e2) = tls_stream if let Err(e2) =
.write_all(&resp) tls_stream.write_all(&resp).await
.await
{ {
tracing::error!( tracing::error!(
"failed to write tls error: {:?}", "failed to write tls error: {:?}",
@ -574,90 +635,43 @@ pub async fn serve_tls(
} }
continue; continue;
} }
let resp = METHOD_REFRESH => {
build_success_response(&msg.header); let resp = build_success_response(&msg.header);
if let Err(e) = if let Err(e) = tls_stream.write_all(&resp).await {
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 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!( tracing::error!(
"failed to write tls response: {:?}", "failed to write tls refresh response: {:?}",
e e
); );
} }
continue;
} }
Err(e) => tracing::error!( METHOD_BINDING => {
"allocate failed (tls): {:?}", 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 e
), );
} }
continue; continue;
} }
_ => {
// default: send 401 challenge let nonce = auth_clone.mint_nonce(&peer);
let nonce = format!("nonce-{}", uuid::Uuid::new_v4());
let resp = build_401_response( let resp = build_401_response(
&msg.header, &msg.header,
"niom-turn.local", auth_clone.realm(),
&nonce, &nonce,
401, 401,
"Unauthorized",
); );
if let Err(e) = tls_stream.write_all(&resp).await { if let Err(e) = tls_stream.write_all(&resp).await {
tracing::error!("failed to write tls 401: {:?}", e); tracing::error!(
"failed to write tls fallback challenge: {:?}",
e
);
}
continue;
}
} }
} else { } else {
tracing::debug!( tracing::debug!(