Introduced AuthManager with signed nonce handling and long-term credential validation.
This commit is contained in:
parent
c77e95afdd
commit
7169ed0d1e
38
README.md
38
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<String>`.
|
||||
- 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<String>` 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
|
||||
-----------------------------------------
|
||||
|
||||
@ -9,5 +9,10 @@
|
||||
"username": "testuser",
|
||||
"password": "secretpassword"
|
||||
}
|
||||
]
|
||||
],
|
||||
"auth": {
|
||||
"realm": "niom-turn.local",
|
||||
"nonce_secret": null,
|
||||
"nonce_ttl_seconds": 300
|
||||
}
|
||||
}
|
||||
|
||||
215
src/auth.rs
215
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<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)
|
||||
pub fn compute_hmac_sha1_bytes(key: &str, data: &[u8]) -> Vec<u8> {
|
||||
use hmac::{Hmac, Mac};
|
||||
|
||||
@ -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<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)]
|
||||
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<CredentialEntry>,
|
||||
/// Authentication behaviour advertised to clients.
|
||||
#[serde(default)]
|
||||
pub auth: AuthOptions,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
709
src/main.rs
709
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<UdpSocket>,
|
||||
creds: InMemoryStore,
|
||||
auth: AuthManager<InMemoryStore>,
|
||||
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);
|
||||
|
||||
38
src/stun.rs
38
src/stun.rs
@ -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.
|
||||
pub fn build_401_response(req: &StunHeader, realm: &str, nonce: &str, _err_code: u16) -> Vec<u8> {
|
||||
/// 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<u8> {
|
||||
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<u8> {
|
||||
pub fn compute_message_integrity(key: &[u8], msg: &[u8]) -> Vec<u8> {
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha1::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.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()));
|
||||
}
|
||||
}
|
||||
|
||||
1126
src/tls.rs
1126
src/tls.rs
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user