use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct NatConfigSpec {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub stun_servers: Vec<String>,
#[serde(default)]
pub turn_servers: Vec<TurnServerSpec>,
#[serde(default)]
pub hole_punch_timeout_secs: u64,
#[serde(default)]
pub stun_refresh_interval_secs: u64,
#[serde(default)]
pub max_candidate_pairs: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relay_server: Option<RelayServerSpec>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct TurnServerSpec {
#[serde(default)]
pub addr: String,
#[serde(default)]
pub username: String,
#[serde(default)]
pub credential: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RelayServerSpec {
#[serde(default)]
pub listen_port: u16,
#[serde(default)]
pub external_addr: String,
#[serde(default)]
pub max_sessions: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_credential: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
pub struct NatCandidateWire {
pub candidate_type: String,
pub address: String,
pub priority: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nat_config_spec_round_trips() {
let spec = NatConfigSpec {
enabled: true,
stun_servers: vec!["stun.l.google.com:19302".into()],
turn_servers: vec![TurnServerSpec {
addr: "turn.example.com:3478".into(),
username: "u".into(),
credential: "p".into(),
}],
hole_punch_timeout_secs: 15,
stun_refresh_interval_secs: 60,
max_candidate_pairs: 10,
relay_server: Some(RelayServerSpec {
listen_port: 3478,
external_addr: "1.2.3.4:3478".into(),
max_sessions: 100,
auth_credential: Some("cluster-secret".into()),
}),
};
let json = serde_json::to_string(&spec).expect("serialize");
let back: NatConfigSpec = serde_json::from_str(&json).expect("deserialize");
assert_eq!(spec, back);
}
#[test]
fn nat_config_spec_empty_object_is_all_default() {
let spec: NatConfigSpec = serde_json::from_str("{}").expect("deserialize");
assert!(!spec.enabled);
assert!(spec.stun_servers.is_empty());
assert!(spec.turn_servers.is_empty());
assert_eq!(spec.hole_punch_timeout_secs, 0);
assert!(spec.relay_server.is_none());
assert_eq!(spec, NatConfigSpec::default());
}
#[test]
fn relay_server_spec_omits_none_credential() {
let spec = RelayServerSpec {
listen_port: 3478,
external_addr: "1.2.3.4:3478".into(),
max_sessions: 50,
auth_credential: None,
};
let json = serde_json::to_string(&spec).expect("serialize");
assert!(
!json.contains("auth_credential"),
"None credential must be skipped: {json}"
);
let back: RelayServerSpec = serde_json::from_str(&json).expect("deserialize");
assert_eq!(spec, back);
}
#[test]
fn nat_candidate_wire_round_trips() {
let c = NatCandidateWire {
candidate_type: "server-reflexive".into(),
address: "203.0.113.5:51820".into(),
priority: 50,
};
let json = serde_json::to_string(&c).expect("serialize");
let back: NatCandidateWire = serde_json::from_str(&json).expect("deserialize");
assert_eq!(c, back);
}
}