From 5e5da0c78c2657b2862f705444a213b06ca8105d Mon Sep 17 00:00:00 2001 From: Keenan Tims Date: Thu, 22 Oct 2026 17:49:12 -0700 Subject: [PATCH] Load daemon configuration from file, environment and CLI Bind addresses and peers are now optional on the command line and merged with the config file. Per-peer timers and detect multiplier are passed through to the session. Multiple bind addresses are accepted by the model but rejected at startup until supported. --- src/bin/rust-bfdd.rs | 107 +++++++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 35 deletions(-) diff --git a/src/bin/rust-bfdd.rs b/src/bin/rust-bfdd.rs index 9ab478d..c29dce0 100644 --- a/src/bin/rust-bfdd.rs +++ b/src/bin/rust-bfdd.rs @@ -1,5 +1,6 @@ use clap::Parser; use futures::future; +use rust_bfd::config::{Config, ConfigError, ResolvedConfig, ResolvedPeer}; use rust_bfd::events::{EventMessageSink, ScriptHookSink, StateChangeEvent}; use rust_bfd::{bfd::*, events::EventMessage}; use rust_bfd::{set_ttl_or_hop_limit, BfdSessionStats}; @@ -10,6 +11,7 @@ use std::{ io::ErrorKind, mem::swap, net::{IpAddr, SocketAddr}, + path::PathBuf, sync::{atomic::Ordering, Arc}, time::{Duration, Instant}, }; @@ -37,12 +39,18 @@ const ECHO_PORT: u16 = 3785; #[derive(Parser, Debug)] #[command(version, about = "BFD (RFC 5880) daemon")] struct Args { - /// Local IP address to bind to and use as the source of BFD control packets - #[arg(short, long, value_name = "IP")] - bind: IpAddr, + /// YAML configuration file [default: /etc/rust-bfd/config.yaml, ignored if absent] + #[arg(short, long, value_name = "FILE", env = "BFD_CONFIG")] + config: Option, - /// Peer IP address to establish a session with (repeatable; must match the bind address family) - #[arg(short, long = "peer", value_name = "IP", required = true)] + /// Local IP address to bind to and use as the source of BFD control packets (repeatable). Replaces any bind + /// addresses from the config file or environment. + #[arg(short, long, value_name = "IP")] + bind: Vec, + + /// Peer IP address to establish a session with, using the default timers (repeatable). Added to the peers from + /// the config file. + #[arg(short, long = "peer", value_name = "IP")] peers: Vec, } @@ -293,7 +301,7 @@ struct BfdSessionHandle { impl BfdSessionHandle { async fn new( local_addr: IpAddr, - remote_addr: IpAddr, + peer: ResolvedPeer, local_discr: BfdDiscriminator, event_dispatcher: EventDispatcherHandle, ) -> io::Result { @@ -307,7 +315,7 @@ impl BfdSessionHandle { new_self.clone(), control_sock, local_addr, - remote_addr, + peer, local_discr, event_dispatcher, ); @@ -418,14 +426,14 @@ impl BfdSession { handle: BfdSessionHandle, control_sock: UdpSocket, local_addr: IpAddr, - remote_addr: IpAddr, + peer: ResolvedPeer, local_discr: BfdDiscriminator, event_dispatcher: EventDispatcherHandle, ) -> Self { let mut rng = rand::thread_rng(); let periodic = BfdPeriodicSenderHandle::new(handle.clone(), time::Duration::from_secs(1).into()); - let configured_desired_min_tx = BfdInterval::from_millis(300); + let configured_desired_min_tx = peer.desired_min_tx; let slow_desired_min_tx = std::cmp::max(configured_desired_min_tx, BfdInterval::from_secs(1)); @@ -446,18 +454,18 @@ impl BfdSession { state: BfdSessionState { control_sock: Arc::new(control_sock), local_ip: local_addr, - peer_addr: remote_addr, + peer_addr: peer.address, session_state: Atomic::new(BfdState::default()), remote_session_state: Atomic::new(BfdState::default()), local_discr, remote_discr: Atomic::new(BfdDiscriminator(0)), local_diag: Atomic::new(BfdDiagnostic::None), desired_min_tx_interval: Atomic::new(slow_desired_min_tx), - required_min_rx_interval: Atomic::new(BfdInterval::from_millis(300)), + required_min_rx_interval: Atomic::new(peer.required_min_rx), remote_min_rx_interval: Atomic::new(BfdInterval::from_millis(300)), demand_mode: Atomic::new(false), remote_demand_mode: Atomic::new(false), - detect_mult: Atomic::new(3), + detect_mult: Atomic::new(peer.detect_mult), auth_type: Atomic::new(BfdAuthType::None), rcv_auth_seq: Atomic::new(0), xmit_auth_seq: Atomic::new(rng.gen()), @@ -975,7 +983,7 @@ impl EventDispatcherHandle { struct Bfdd { local: SocketAddr, - peers: Vec, + peers: Vec, sessions_by_ip: HashMap<(IpAddr, IpAddr), BfdDiscriminator>, sessions_by_discr: HashMap, @@ -985,24 +993,18 @@ struct Bfdd { } impl Bfdd { - async fn new(args: Args) -> Result> { - let local = SocketAddr::new(args.bind, CONTROL_PORT); - if let Some(peer) = args - .peers - .iter() - .find(|p| p.is_ipv4() != args.bind.is_ipv4()) - { - return Err(format!( - "peer {peer} is not the same address family as bind address {}", - args.bind - ) - .into()); - } - let peers = args - .peers - .into_iter() - .map(|ip| SocketAddr::new(ip, CONTROL_PORT)) - .collect(); + async fn new(config: ResolvedConfig) -> Result> { + let local = match config.bind.as_slice() { + [addr] => SocketAddr::new(*addr, CONTROL_PORT), + addrs => { + return Err(format!( + "{} bind addresses configured, but only one is currently supported", + addrs.len() + ) + .into()) + } + }; + let peers = config.peers; let sessions_by_ip = HashMap::new(); let sessions_by_discr = HashMap::new(); @@ -1016,7 +1018,7 @@ impl Bfdd { let event_dispatcher = EventDispatcherHandle::new().await; event_dispatcher - .add_sink(Box::new(ScriptHookSink::new())) + .add_sink(Box::new(ScriptHookSink::with_script_dir(config.hooks_dir))) .await; Ok(Self { @@ -1037,14 +1039,14 @@ impl Bfdd { } let session = BfdSessionHandle::new( self.local.ip(), - peer.ip(), + *peer, local_discr, self.event_dispatcher.clone(), ) .await?; self.sessions_by_ip - .insert((self.local.ip(), peer.ip()), local_discr); + .insert((self.local.ip(), peer.address), local_discr); self.sessions_by_discr.insert(local_discr, session); } Ok(()) @@ -1156,6 +1158,13 @@ impl Bfdd { } } +fn load_config(args: Args) -> Result { + let mut config = Config::load(args.config.as_deref())?; + config.apply_env(|name| std::env::var(name).ok())?; + config.apply_cli(args.bind, args.peers); + config.resolve() +} + #[tokio::main] async fn main() -> io::Result<()> { let args = Args::parse(); @@ -1164,8 +1173,36 @@ async fn main() -> io::Result<()> { .format_timestamp_micros() .init(); - match Bfdd::new(args).await { + let config = load_config(args).map_err(|e| std::io::Error::other(e.to_string()))?; + if config.peers.is_empty() { + warn!("No peers configured"); + } + + match Bfdd::new(config).await { Ok(bfdd) => bfdd.run().await, Err(e) => Err(std::io::Error::other(e.to_string())), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn repeated_bind_and_peer_flags() { + let args = Args::try_parse_from([ + "rust-bfdd", + "-b", + "192.0.2.1", + "--bind", + "2001:db8::1", + "-p", + "192.0.2.2", + "--peer", + "2001:db8::2", + ]) + .unwrap(); + assert_eq!(args.bind.len(), 2); + assert_eq!(args.peers.len(), 2); + } +}