Add turn client for testing. More debugging output.
This commit is contained in:
parent
abf9b87659
commit
b9ff93b774
97
src/auth.rs
97
src/auth.rs
@ -93,6 +93,47 @@ pub struct AuthManager<S: CredentialStore + Clone> {
|
|||||||
settings: AuthSettings,
|
settings: AuthSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_signed_bytes_adjusted(
|
||||||
|
msg: &StunMessage,
|
||||||
|
mi_offset: usize,
|
||||||
|
mi_len: usize,
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
|
if mi_len != 20 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mi_end = mi_offset.checked_add(4 + mi_len)?;
|
||||||
|
if mi_end > msg.raw.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut signed = msg.raw[..mi_end].to_vec();
|
||||||
|
let len = (mi_end - 20) as u16;
|
||||||
|
signed[2..4].copy_from_slice(&len.to_be_bytes());
|
||||||
|
|
||||||
|
let value_start = mi_offset + 4;
|
||||||
|
signed[value_start..value_start + mi_len].fill(0);
|
||||||
|
Some(signed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_signed_bytes_len_preserved(
|
||||||
|
msg: &StunMessage,
|
||||||
|
mi_offset: usize,
|
||||||
|
mi_len: usize,
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
|
if mi_len != 20 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mi_end = mi_offset.checked_add(4 + mi_len)?;
|
||||||
|
if mi_end > msg.raw.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut signed = msg.raw[..mi_end].to_vec();
|
||||||
|
let value_start = mi_offset + 4;
|
||||||
|
signed[value_start..value_start + mi_len].fill(0);
|
||||||
|
Some(signed)
|
||||||
|
}
|
||||||
|
|
||||||
impl<S: CredentialStore + Clone> Clone for AuthManager<S> {
|
impl<S: CredentialStore + Clone> Clone for AuthManager<S> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -212,6 +253,62 @@ impl<S: CredentialStore + Clone> AuthManager<S> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No acceptance without MI validation. Emit detailed diagnostics.
|
// No acceptance without MI validation. Emit detailed diagnostics.
|
||||||
|
// Keep logs compact by default to avoid journald truncation.
|
||||||
|
// Set NIOM_TURN_DEBUG_AUTH_HEX=1 for a summary, and NIOM_TURN_DEBUG_AUTH_HEX_FULL=1 for full raw/signed hex.
|
||||||
|
if std::env::var_os("NIOM_TURN_DEBUG_AUTH_HEX").is_some() {
|
||||||
|
let mi = find_message_integrity(msg);
|
||||||
|
let mi_end = mi.map(|a| a.offset + 4 + a.value.len()).unwrap_or(0);
|
||||||
|
|
||||||
|
let mut attrs = Vec::new();
|
||||||
|
for a in &msg.attributes {
|
||||||
|
attrs.push(format!(
|
||||||
|
"t=0x{:04x} len={} off={} v={}",
|
||||||
|
a.typ,
|
||||||
|
a.value.len(),
|
||||||
|
a.offset,
|
||||||
|
hex::encode(&a.value)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let signed_adj_hex = mi
|
||||||
|
.and_then(|a| build_signed_bytes_adjusted(msg, a.offset, a.value.len()))
|
||||||
|
.map(hex::encode);
|
||||||
|
let signed_len_hex = mi
|
||||||
|
.and_then(|a| build_signed_bytes_len_preserved(msg, a.offset, a.value.len()))
|
||||||
|
.map(hex::encode);
|
||||||
|
|
||||||
|
if std::env::var_os("NIOM_TURN_DEBUG_AUTH_HEX_FULL").is_some() {
|
||||||
|
warn!(
|
||||||
|
"auth debug dump FULL peer={} msg_type=0x{:04x} raw_len={} raw={} mi_end={} attrs=[{}] signed_adj={:?} signed_len_preserved={:?}",
|
||||||
|
peer,
|
||||||
|
msg.header.msg_type,
|
||||||
|
msg.raw.len(),
|
||||||
|
hex::encode(&msg.raw),
|
||||||
|
mi_end,
|
||||||
|
attrs.join(" | "),
|
||||||
|
signed_adj_hex,
|
||||||
|
signed_len_hex
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let mi_hex = mi.map(|a| hex::encode(&a.value));
|
||||||
|
warn!(
|
||||||
|
"auth debug dump peer={} msg_type=0x{:04x} raw_len={} mi_end={} mi={:?} attrs=[{}]",
|
||||||
|
peer,
|
||||||
|
msg.header.msg_type,
|
||||||
|
msg.raw.len(),
|
||||||
|
mi_end,
|
||||||
|
mi_hex,
|
||||||
|
attrs.join(" | ")
|
||||||
|
);
|
||||||
|
warn!(
|
||||||
|
"auth debug signed (truncated) peer={} signed_adj_prefix={:?} signed_len_preserved_prefix={:?}",
|
||||||
|
peer,
|
||||||
|
signed_adj_hex.as_ref().map(|s| s.chars().take(160).collect::<String>()),
|
||||||
|
signed_len_hex.as_ref().map(|s| s.chars().take(160).collect::<String>())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mi_attr = find_message_integrity(msg).map(|a| hex::encode(&a.value));
|
let mi_attr = find_message_integrity(msg).map(|a| hex::encode(&a.value));
|
||||||
let mi_long_adj = compute_message_integrity_adjusted(msg, &key).map(hex::encode);
|
let mi_long_adj = compute_message_integrity_adjusted(msg, &key).map(hex::encode);
|
||||||
let mi_long_len = compute_mi_len_preserved(msg, &key).map(hex::encode);
|
let mi_long_len = compute_mi_len_preserved(msg, &key).map(hex::encode);
|
||||||
|
|||||||
340
src/bin/turn_client.rs
Normal file
340
src/bin/turn_client.rs
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
use anyhow::{anyhow, bail, Context};
|
||||||
|
use crc32fast::Hasher;
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha1::Sha1;
|
||||||
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const MAGIC_COOKIE: u32 = 0x2112A442;
|
||||||
|
|
||||||
|
// STUN/TURN attribute types (RFC 5389 / RFC 5766)
|
||||||
|
const ATTR_USERNAME: u16 = 0x0006;
|
||||||
|
const ATTR_MESSAGE_INTEGRITY: u16 = 0x0008;
|
||||||
|
const ATTR_ERROR_CODE: u16 = 0x0009;
|
||||||
|
const ATTR_REALM: u16 = 0x0014;
|
||||||
|
const ATTR_NONCE: u16 = 0x0015;
|
||||||
|
const ATTR_REQUESTED_TRANSPORT: u16 = 0x0019;
|
||||||
|
const ATTR_FINGERPRINT: u16 = 0x8028;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Opt {
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
user: String,
|
||||||
|
pass: String,
|
||||||
|
realm: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"turn_client --host <host> --port <port> --user <u> --pass <p> --realm <realm>\n\nExample:\n cargo run --bin turn_client -- --host ghostnet.selfhost.eu --port 3478 --user testuser --pass secretpassword --realm ghostnet.selfhost.eu\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args() -> anyhow::Result<Opt> {
|
||||||
|
let mut host: Option<String> = None;
|
||||||
|
let mut port: Option<u16> = None;
|
||||||
|
let mut user: Option<String> = None;
|
||||||
|
let mut pass: Option<String> = None;
|
||||||
|
let mut realm: Option<String> = None;
|
||||||
|
|
||||||
|
let mut it = std::env::args().skip(1);
|
||||||
|
while let Some(a) = it.next() {
|
||||||
|
match a.as_str() {
|
||||||
|
"--host" => host = Some(it.next().ok_or_else(|| anyhow!("missing value for --host"))?),
|
||||||
|
"--port" => {
|
||||||
|
let p = it.next().ok_or_else(|| anyhow!("missing value for --port"))?;
|
||||||
|
port = Some(p.parse::<u16>().context("invalid --port")?);
|
||||||
|
}
|
||||||
|
"--user" => user = Some(it.next().ok_or_else(|| anyhow!("missing value for --user"))?),
|
||||||
|
"--pass" => pass = Some(it.next().ok_or_else(|| anyhow!("missing value for --pass"))?),
|
||||||
|
"--realm" => realm = Some(it.next().ok_or_else(|| anyhow!("missing value for --realm"))?),
|
||||||
|
"-h" | "--help" => bail!(usage()),
|
||||||
|
other => bail!("unknown arg: {other}\n\n{}", usage()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Opt {
|
||||||
|
host: host.ok_or_else(|| anyhow!("--host required"))?,
|
||||||
|
port: port.unwrap_or(3478),
|
||||||
|
user: user.ok_or_else(|| anyhow!("--user required"))?,
|
||||||
|
pass: pass.ok_or_else(|| anyhow!("--pass required"))?,
|
||||||
|
realm: realm.ok_or_else(|| anyhow!("--realm required"))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let opt = match parse_args() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{e}\n");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let target = resolve_target(&opt.host, opt.port)?;
|
||||||
|
println!("target={target}");
|
||||||
|
|
||||||
|
let sock = UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
sock.connect(target).await?;
|
||||||
|
|
||||||
|
// 1) Allocate without auth to get 401 + realm/nonce
|
||||||
|
let trans_id_1 = random_trans_id();
|
||||||
|
let req1 = build_allocate_request_unauth(trans_id_1);
|
||||||
|
println!("req1 raw={}", hex::encode(&req1));
|
||||||
|
let resp1 = send_and_recv(&sock, &req1).await?;
|
||||||
|
let parsed1 = parse_stun(&resp1)?;
|
||||||
|
println!("resp1 type=0x{:04x} len={} raw={}", parsed1.msg_type, parsed1.length, hex::encode(&resp1));
|
||||||
|
|
||||||
|
let (code1, realm1, nonce1) = extract_error_realm_nonce(&parsed1);
|
||||||
|
println!("resp1 error={code1:?} realm={realm1:?} nonce_len={}", nonce1.as_deref().map(|n| n.len()).unwrap_or(0));
|
||||||
|
|
||||||
|
let nonce = nonce1.ok_or_else(|| anyhow!("no NONCE in first response"))?;
|
||||||
|
let realm = realm1.unwrap_or_else(|| opt.realm.clone());
|
||||||
|
|
||||||
|
// 2) Allocate with long-term auth (USERNAME/REALM/NONCE + MI + FINGERPRINT)
|
||||||
|
let trans_id_2 = random_trans_id();
|
||||||
|
let key = long_term_key_md5(&opt.user, &realm, &opt.pass);
|
||||||
|
println!("key_md5={}", hex::encode(&key));
|
||||||
|
|
||||||
|
let req2 = build_allocate_request_auth(trans_id_2, &opt.user, &realm, &nonce, &key, true);
|
||||||
|
println!("req2 raw={}", hex::encode(&req2));
|
||||||
|
let resp2 = send_and_recv(&sock, &req2).await?;
|
||||||
|
let parsed2 = parse_stun(&resp2)?;
|
||||||
|
println!("resp2 type=0x{:04x} len={} raw={}", parsed2.msg_type, parsed2.length, hex::encode(&resp2));
|
||||||
|
|
||||||
|
let (code2, _realm2, _nonce2) = extract_error_realm_nonce(&parsed2);
|
||||||
|
if let Some(code) = code2 {
|
||||||
|
println!("resp2 error={code}");
|
||||||
|
} else {
|
||||||
|
println!("resp2 looks non-error (success?)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_target(host: &str, port: u16) -> anyhow::Result<SocketAddr> {
|
||||||
|
let mut addrs = (host, port)
|
||||||
|
.to_socket_addrs()
|
||||||
|
.with_context(|| format!("DNS resolve failed for {host}:{port}"))?;
|
||||||
|
addrs
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("no socket addresses for {host}:{port}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_trans_id() -> [u8; 12] {
|
||||||
|
let u = Uuid::new_v4();
|
||||||
|
let b = u.as_bytes();
|
||||||
|
let mut out = [0u8; 12];
|
||||||
|
out.copy_from_slice(&b[0..12]);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_and_recv(sock: &UdpSocket, req: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||||||
|
sock.send(req).await?;
|
||||||
|
let mut buf = [0u8; 2048];
|
||||||
|
let n = tokio::time::timeout(Duration::from_secs(2), sock.recv(&mut buf)).await??;
|
||||||
|
Ok(buf[..n].to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_allocate_request_unauth(trans_id: [u8; 12]) -> Vec<u8> {
|
||||||
|
let msg_type: u16 = 0x0003; // Allocate request
|
||||||
|
let mut buf = Vec::with_capacity(256);
|
||||||
|
write_header(&mut buf, msg_type, trans_id);
|
||||||
|
|
||||||
|
// REQUESTED-TRANSPORT (UDP = 17)
|
||||||
|
append_attr(&mut buf, ATTR_REQUESTED_TRANSPORT, &[17, 0, 0, 0]);
|
||||||
|
|
||||||
|
// No USERNAME/REALM/NONCE/MI
|
||||||
|
finalize_len(&mut buf);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_allocate_request_auth(
|
||||||
|
trans_id: [u8; 12],
|
||||||
|
user: &str,
|
||||||
|
realm: &str,
|
||||||
|
nonce: &str,
|
||||||
|
key: &[u8],
|
||||||
|
fingerprint: bool,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let msg_type: u16 = 0x0003; // Allocate request
|
||||||
|
let mut buf = Vec::with_capacity(512);
|
||||||
|
write_header(&mut buf, msg_type, trans_id);
|
||||||
|
|
||||||
|
append_attr(&mut buf, ATTR_REQUESTED_TRANSPORT, &[17, 0, 0, 0]);
|
||||||
|
append_attr(&mut buf, ATTR_USERNAME, user.as_bytes());
|
||||||
|
append_attr(&mut buf, ATTR_REALM, realm.as_bytes());
|
||||||
|
append_attr(&mut buf, ATTR_NONCE, nonce.as_bytes());
|
||||||
|
|
||||||
|
// MESSAGE-INTEGRITY, then optionally FINGERPRINT after it.
|
||||||
|
append_message_integrity_rfc(&mut buf, key);
|
||||||
|
|
||||||
|
if fingerprint {
|
||||||
|
append_fingerprint(&mut buf);
|
||||||
|
} else {
|
||||||
|
finalize_len(&mut buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_header(buf: &mut Vec<u8>, msg_type: u16, trans_id: [u8; 12]) {
|
||||||
|
buf.extend_from_slice(&msg_type.to_be_bytes());
|
||||||
|
buf.extend_from_slice(&0u16.to_be_bytes()); // length placeholder
|
||||||
|
buf.extend_from_slice(&MAGIC_COOKIE.to_be_bytes());
|
||||||
|
buf.extend_from_slice(&trans_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finalize_len(buf: &mut Vec<u8>) {
|
||||||
|
let len = (buf.len() - 20) as u16;
|
||||||
|
buf[2..4].copy_from_slice(&len.to_be_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_attr(buf: &mut Vec<u8>, typ: u16, val: &[u8]) {
|
||||||
|
buf.extend_from_slice(&typ.to_be_bytes());
|
||||||
|
buf.extend_from_slice(&(val.len() as u16).to_be_bytes());
|
||||||
|
buf.extend_from_slice(val);
|
||||||
|
while (buf.len() % 4) != 0 {
|
||||||
|
buf.push(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn long_term_key_md5(user: &str, realm: &str, pass: &str) -> [u8; 16] {
|
||||||
|
let s = format!("{user}:{realm}:{pass}");
|
||||||
|
let d = md5::compute(s.as_bytes());
|
||||||
|
d.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RFC 5389 style MI computation:
|
||||||
|
/// - HMAC is computed over the message up to (and including) the MI attribute
|
||||||
|
/// - In the bytes being MAC'ed, the STUN header length is set to the length up to MI
|
||||||
|
/// - The MI value bytes are treated as zero for the MAC
|
||||||
|
fn append_message_integrity_rfc(buf: &mut Vec<u8>, key: &[u8]) {
|
||||||
|
const HMAC_LEN: usize = 20;
|
||||||
|
|
||||||
|
// Add MI with zero placeholder
|
||||||
|
buf.extend_from_slice(&ATTR_MESSAGE_INTEGRITY.to_be_bytes());
|
||||||
|
buf.extend_from_slice(&(HMAC_LEN as u16).to_be_bytes());
|
||||||
|
let mi_pos = buf.len();
|
||||||
|
buf.extend_from_slice(&[0u8; HMAC_LEN]);
|
||||||
|
while (buf.len() % 4) != 0 {
|
||||||
|
buf.push(0);
|
||||||
|
}
|
||||||
|
let mi_end = buf.len();
|
||||||
|
|
||||||
|
// Compute HMAC over header..mi_end, but with adjusted length and MI bytes set to 0.
|
||||||
|
let mut signed = buf[..mi_end].to_vec();
|
||||||
|
let adjusted_len = (mi_end - 20) as u16;
|
||||||
|
signed[2..4].copy_from_slice(&adjusted_len.to_be_bytes());
|
||||||
|
for b in &mut signed[mi_pos..mi_pos + HMAC_LEN] {
|
||||||
|
*b = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mac = Hmac::<Sha1>::new_from_slice(key).expect("HMAC key");
|
||||||
|
mac.update(&signed);
|
||||||
|
let h = mac.finalize().into_bytes();
|
||||||
|
buf[mi_pos..mi_pos + HMAC_LEN].copy_from_slice(&h[..HMAC_LEN]);
|
||||||
|
|
||||||
|
// Important: leave final length setting to caller (because they may append FINGERPRINT).
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_fingerprint(buf: &mut Vec<u8>) {
|
||||||
|
// RFC 5389 section 15.7 (matches the server's behavior):
|
||||||
|
// - FINGERPRINT must be last.
|
||||||
|
// - Header length used for CRC is the final message length (including fingerprint).
|
||||||
|
// - CRC input excludes the fingerprint attribute itself.
|
||||||
|
let fp_attr_offset = buf.len();
|
||||||
|
buf.extend_from_slice(&ATTR_FINGERPRINT.to_be_bytes());
|
||||||
|
buf.extend_from_slice(&(4u16).to_be_bytes());
|
||||||
|
let fp_value_pos = buf.len();
|
||||||
|
buf.extend_from_slice(&[0u8; 4]);
|
||||||
|
|
||||||
|
// Update header length to include fingerprint.
|
||||||
|
finalize_len(buf);
|
||||||
|
|
||||||
|
let mut hasher = Hasher::new();
|
||||||
|
hasher.update(&buf[..fp_attr_offset]);
|
||||||
|
let crc = hasher.finalize() ^ 0x5354554e;
|
||||||
|
buf[fp_value_pos..fp_value_pos + 4].copy_from_slice(&crc.to_be_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Parsed {
|
||||||
|
msg_type: u16,
|
||||||
|
length: u16,
|
||||||
|
trans_id: [u8; 12],
|
||||||
|
attrs: Vec<(u16, Vec<u8>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_stun(buf: &[u8]) -> anyhow::Result<Parsed> {
|
||||||
|
if buf.len() < 20 {
|
||||||
|
bail!("too short for STUN header");
|
||||||
|
}
|
||||||
|
let msg_type = u16::from_be_bytes([buf[0], buf[1]]);
|
||||||
|
let length = u16::from_be_bytes([buf[2], buf[3]]);
|
||||||
|
let cookie = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
|
||||||
|
if cookie != MAGIC_COOKIE {
|
||||||
|
bail!("invalid magic cookie: 0x{cookie:08x}");
|
||||||
|
}
|
||||||
|
let mut trans_id = [0u8; 12];
|
||||||
|
trans_id.copy_from_slice(&buf[8..20]);
|
||||||
|
|
||||||
|
let total = 20usize + (length as usize);
|
||||||
|
if buf.len() < total {
|
||||||
|
bail!("buffer shorter than STUN length: buf={} total={total}", buf.len());
|
||||||
|
}
|
||||||
|
let mut off = 20usize;
|
||||||
|
let mut attrs = Vec::new();
|
||||||
|
while off + 4 <= total {
|
||||||
|
let typ = u16::from_be_bytes([buf[off], buf[off + 1]]);
|
||||||
|
let l = u16::from_be_bytes([buf[off + 2], buf[off + 3]]) as usize;
|
||||||
|
off += 4;
|
||||||
|
if off + l > total {
|
||||||
|
bail!("attr overflow typ=0x{typ:04x} len={l}");
|
||||||
|
}
|
||||||
|
let val = buf[off..off + l].to_vec();
|
||||||
|
attrs.push((typ, val));
|
||||||
|
off += l;
|
||||||
|
off += (4 - (l % 4)) % 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Parsed {
|
||||||
|
msg_type,
|
||||||
|
length,
|
||||||
|
trans_id,
|
||||||
|
attrs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_error_realm_nonce(p: &Parsed) -> (Option<u16>, Option<String>, Option<String>) {
|
||||||
|
let mut code: Option<u16> = None;
|
||||||
|
let mut realm: Option<String> = None;
|
||||||
|
let mut nonce: Option<String> = None;
|
||||||
|
|
||||||
|
for (typ, val) in &p.attrs {
|
||||||
|
match *typ {
|
||||||
|
ATTR_ERROR_CODE => {
|
||||||
|
if val.len() >= 4 {
|
||||||
|
let class = val[2] as u16;
|
||||||
|
let number = val[3] as u16;
|
||||||
|
code = Some(class * 100 + number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ATTR_REALM => {
|
||||||
|
if let Ok(s) = std::str::from_utf8(val) {
|
||||||
|
realm = Some(s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ATTR_NONCE => {
|
||||||
|
if let Ok(s) = std::str::from_utf8(val) {
|
||||||
|
nonce = Some(s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(code, realm, nonce)
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
//! Configuration loader for server bind addresses, TLS artifacts, and seed credentials.
|
//! Configuration loader for server bind addresses, TLS artifacts, and seed credentials.
|
||||||
//! Backlog: hot-reload support, secret injection, and environment overrides per deployment.
|
//! Backlog: hot-reload support, secret injection, and environment overrides per deployment.
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
fn default_realm() -> String {
|
fn default_realm() -> String {
|
||||||
@ -31,13 +31,13 @@ fn default_enable_tls() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct CredentialEntry {
|
pub struct CredentialEntry {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct AuthOptions {
|
pub struct AuthOptions {
|
||||||
/// STUN/TURN realm advertised to clients when issuing challenges.
|
/// STUN/TURN realm advertised to clients when issuing challenges.
|
||||||
#[serde(default = "default_realm")]
|
#[serde(default = "default_realm")]
|
||||||
@ -72,7 +72,7 @@ impl Default for AuthOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct ServerOptions {
|
pub struct ServerOptions {
|
||||||
/// Listen address (legacy/default), e.g. "0.0.0.0:3478".
|
/// Listen address (legacy/default), e.g. "0.0.0.0:3478".
|
||||||
/// If `udp_bind` / `tcp_bind` are not set, this value is used.
|
/// If `udp_bind` / `tcp_bind` are not set, this value is used.
|
||||||
@ -99,7 +99,7 @@ pub struct ServerOptions {
|
|||||||
pub tls_key: Option<String>,
|
pub tls_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||||
pub struct RelayOptions {
|
pub struct RelayOptions {
|
||||||
/// Optional UDP relay port range. If set, allocations bind within this range.
|
/// Optional UDP relay port range. If set, allocations bind within this range.
|
||||||
/// If omitted, OS chooses an ephemeral port.
|
/// If omitted, OS chooses an ephemeral port.
|
||||||
@ -119,7 +119,7 @@ pub struct RelayOptions {
|
|||||||
pub advertised_ip: Option<String>,
|
pub advertised_ip: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||||
pub struct LoggingOptions {
|
pub struct LoggingOptions {
|
||||||
/// Default tracing directive (overridable via RUST_LOG).
|
/// Default tracing directive (overridable via RUST_LOG).
|
||||||
/// Example: "warn,niom_turn=info".
|
/// Example: "warn,niom_turn=info".
|
||||||
@ -127,7 +127,7 @@ pub struct LoggingOptions {
|
|||||||
pub default_directive: Option<String>,
|
pub default_directive: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||||
pub struct LimitsOptions {
|
pub struct LimitsOptions {
|
||||||
/// Max concurrent allocations per source IP (across different source ports).
|
/// Max concurrent allocations per source IP (across different source ports).
|
||||||
/// If omitted, unlimited.
|
/// If omitted, unlimited.
|
||||||
@ -159,7 +159,7 @@ pub struct LimitsOptions {
|
|||||||
pub binding_burst: Option<u32>,
|
pub binding_burst: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Server options
|
/// Server options
|
||||||
pub server: ServerOptions,
|
pub server: ServerOptions,
|
||||||
|
|||||||
15
src/main.rs
15
src/main.rs
@ -91,6 +91,21 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
);
|
);
|
||||||
info!("logging.default_directive={}", log_directive);
|
info!("logging.default_directive={}", log_directive);
|
||||||
|
|
||||||
|
// Debug-only: dump the fully loaded config (including secrets) to prove what we are using.
|
||||||
|
// Enable explicitly: NIOM_TURN_DEBUG_CONFIG=1
|
||||||
|
// Optional pretty JSON: NIOM_TURN_DEBUG_CONFIG_JSON=1
|
||||||
|
if std::env::var_os("NIOM_TURN_DEBUG_CONFIG").is_some() {
|
||||||
|
warn!("DEBUG CONFIG DUMP ENABLED (includes secrets). Disable after verification.");
|
||||||
|
if std::env::var_os("NIOM_TURN_DEBUG_CONFIG_JSON").is_some() {
|
||||||
|
match serde_json::to_string_pretty(&cfg) {
|
||||||
|
Ok(s) => warn!("config dump (json) source={}\n{}", cfg_source, s),
|
||||||
|
Err(e) => warn!("config dump (json) failed: {:?}", e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("config dump (debug) source={} cfg={:?}", cfg_source, cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let udp_bind = cfg
|
let udp_bind = cfg
|
||||||
.server
|
.server
|
||||||
.udp_bind
|
.udp_bind
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user