use std::net::SocketAddr;
use serde::Deserialize;
use tokio::net::UdpSocket;
use tokio::sync::Mutex;
pub const DEFAULT_INGRESS_PORT: u16 = 5820;
#[derive(Debug, Default, Deserialize)]
struct RawRoot {
#[serde(default)]
radio: RawRadio,
}
#[derive(Debug, Default, Deserialize)]
struct RawRadio {
#[serde(default)]
tunnel: RawTunnel,
}
#[derive(Debug, Default, Deserialize)]
struct RawTunnel {
#[serde(default)]
rx_port: Option<u16>,
}
#[must_use]
pub fn ingress_port_from_yaml(text: &str) -> u16 {
let raw: RawRoot = serde_norway::from_str(text).unwrap_or_default();
raw.radio
.tunnel
.rx_port
.filter(|p| *p != 0)
.unwrap_or(DEFAULT_INGRESS_PORT)
}
#[must_use]
pub fn ingress_port_from(path: &std::path::Path) -> u16 {
match std::fs::read_to_string(path) {
Ok(text) => ingress_port_from_yaml(&text),
Err(_) => DEFAULT_INGRESS_PORT,
}
}
#[must_use]
pub fn ingress_port() -> u16 {
ingress_port_from(&crate::aux_ports::config_path())
}
pub struct ConfigTunnelIngest {
target: SocketAddr,
sock: Mutex<Option<UdpSocket>>,
}
impl ConfigTunnelIngest {
#[must_use]
pub fn new(port: u16) -> Self {
Self {
target: SocketAddr::from(([127, 0, 0, 1], port)),
sock: Mutex::new(None),
}
}
#[must_use]
pub fn at_configured_port() -> Self {
Self::new(ingress_port())
}
#[must_use]
pub fn target_port(&self) -> u16 {
self.target.port()
}
pub async fn send(&self, frame: &[u8]) -> bool {
let mut guard = self.sock.lock().await;
if guard.is_none() {
match UdpSocket::bind("127.0.0.1:0").await {
Ok(s) => *guard = Some(s),
Err(e) => {
tracing::debug!(error = %e, "config_tunnel_ingest_bind_failed");
return false;
}
}
}
let sock = guard.as_ref().expect("socket bound above");
match sock.send_to(frame, self.target).await {
Ok(_) => true,
Err(e) => {
tracing::debug!(error = %e, port = self.target.port(), "config_tunnel_ingest_send_failed");
*guard = None;
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_port_is_used_when_nothing_is_configured() {
assert_eq!(ingress_port_from_yaml(""), DEFAULT_INGRESS_PORT);
assert_eq!(
ingress_port_from_yaml("network:\n hostname: node\n"),
DEFAULT_INGRESS_PORT
);
}
#[test]
fn an_operator_override_is_honoured() {
assert_eq!(
ingress_port_from_yaml("radio:\n tunnel:\n rx_port: 6001\n"),
6001
);
}
#[test]
fn port_zero_and_malformed_yaml_fall_back() {
assert_eq!(
ingress_port_from_yaml("radio:\n tunnel:\n rx_port: 0\n"),
DEFAULT_INGRESS_PORT
);
assert_eq!(
ingress_port_from_yaml("radio: [not a map"),
DEFAULT_INGRESS_PORT
);
}
#[test]
fn a_missing_file_yields_the_default() {
assert_eq!(
ingress_port_from(std::path::Path::new("/nonexistent/ados/config.yaml")),
DEFAULT_INGRESS_PORT
);
}
#[tokio::test]
async fn a_forwarded_frame_arrives_verbatim() {
let listener = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let ingest = ConfigTunnelIngest::new(listener.local_addr().unwrap().port());
let frame = crate::aux_mux::encode(crate::aux_mux::AuxChannel::ConfigTunnel, b"chunk")
.expect("within budget");
assert!(ingest.send(&frame).await);
let mut buf = [0u8; 256];
let (n, _) = listener.recv_from(&mut buf).await.unwrap();
assert_eq!(&buf[..n], frame.as_slice());
}
#[tokio::test]
async fn forwarding_with_no_service_listening_returns_promptly() {
let ingest = ConfigTunnelIngest::new(1);
let started = std::time::Instant::now();
let _ = ingest.send(b"frame").await;
assert!(started.elapsed() < std::time::Duration::from_secs(1));
}
}