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,
|
||||
}
|
||||
|
||||
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> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
@ -212,6 +253,62 @@ impl<S: CredentialStore + Clone> AuthManager<S> {
|
||||
}
|
||||
|
||||
// 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_long_adj = compute_message_integrity_adjusted(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.
|
||||
//! Backlog: hot-reload support, secret injection, and environment overrides per deployment.
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
fn default_realm() -> String {
|
||||
@ -31,13 +31,13 @@ fn default_enable_tls() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct CredentialEntry {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct AuthOptions {
|
||||
/// STUN/TURN realm advertised to clients when issuing challenges.
|
||||
#[serde(default = "default_realm")]
|
||||
@ -72,7 +72,7 @@ impl Default for AuthOptions {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ServerOptions {
|
||||
/// Listen address (legacy/default), e.g. "0.0.0.0:3478".
|
||||
/// If `udp_bind` / `tcp_bind` are not set, this value is used.
|
||||
@ -99,7 +99,7 @@ pub struct ServerOptions {
|
||||
pub tls_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
pub struct RelayOptions {
|
||||
/// Optional UDP relay port range. If set, allocations bind within this range.
|
||||
/// If omitted, OS chooses an ephemeral port.
|
||||
@ -119,7 +119,7 @@ pub struct RelayOptions {
|
||||
pub advertised_ip: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
pub struct LoggingOptions {
|
||||
/// Default tracing directive (overridable via RUST_LOG).
|
||||
/// Example: "warn,niom_turn=info".
|
||||
@ -127,7 +127,7 @@ pub struct LoggingOptions {
|
||||
pub default_directive: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
pub struct LimitsOptions {
|
||||
/// Max concurrent allocations per source IP (across different source ports).
|
||||
/// If omitted, unlimited.
|
||||
@ -159,7 +159,7 @@ pub struct LimitsOptions {
|
||||
pub binding_burst: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Config {
|
||||
/// Server options
|
||||
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);
|
||||
|
||||
// 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
|
||||
.server
|
||||
.udp_bind
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user