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.
This commit is contained in:
ktims committed 2026-10-22 17:49:12 -07:00
1 parent ea56afd2f2
commit 5e5da0c78c
1 file changed
+69 -32
+69 -32
View File
@@ -1,5 +1,6 @@
use clap::Parser; use clap::Parser;
use futures::future; use futures::future;
use rust_bfd::config::{Config, ConfigError, ResolvedConfig, ResolvedPeer};
use rust_bfd::events::{EventMessageSink, ScriptHookSink, StateChangeEvent}; use rust_bfd::events::{EventMessageSink, ScriptHookSink, StateChangeEvent};
use rust_bfd::{bfd::*, events::EventMessage}; use rust_bfd::{bfd::*, events::EventMessage};
use rust_bfd::{set_ttl_or_hop_limit, BfdSessionStats}; use rust_bfd::{set_ttl_or_hop_limit, BfdSessionStats};
@@ -10,6 +11,7 @@ use std::{
io::ErrorKind, io::ErrorKind,
mem::swap, mem::swap,
net::{IpAddr, SocketAddr}, net::{IpAddr, SocketAddr},
path::PathBuf,
sync::{atomic::Ordering, Arc}, sync::{atomic::Ordering, Arc},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@@ -37,12 +39,18 @@ const ECHO_PORT: u16 = 3785;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about = "BFD (RFC 5880) daemon")] #[command(version, about = "BFD (RFC 5880) daemon")]
struct Args { struct Args {
/// Local IP address to bind to and use as the source of BFD control packets /// YAML configuration file [default: /etc/rust-bfd/config.yaml, ignored if absent]
#[arg(short, long, value_name = "IP")] #[arg(short, long, value_name = "FILE", env = "BFD_CONFIG")]
bind: IpAddr, config: Option<PathBuf>,
/// Peer IP address to establish a session with (repeatable; must match the bind address family) /// Local IP address to bind to and use as the source of BFD control packets (repeatable). Replaces any bind
#[arg(short, long = "peer", value_name = "IP", required = true)] /// addresses from the config file or environment.
#[arg(short, long, value_name = "IP")]
bind: Vec<IpAddr>,
/// 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<IpAddr>, peers: Vec<IpAddr>,
} }
@@ -293,7 +301,7 @@ struct BfdSessionHandle {
impl BfdSessionHandle { impl BfdSessionHandle {
async fn new( async fn new(
local_addr: IpAddr, local_addr: IpAddr,
remote_addr: IpAddr, peer: ResolvedPeer,
local_discr: BfdDiscriminator, local_discr: BfdDiscriminator,
event_dispatcher: EventDispatcherHandle, event_dispatcher: EventDispatcherHandle,
) -> io::Result<Self> { ) -> io::Result<Self> {
@@ -307,7 +315,7 @@ impl BfdSessionHandle {
new_self.clone(), new_self.clone(),
control_sock, control_sock,
local_addr, local_addr,
remote_addr, peer,
local_discr, local_discr,
event_dispatcher, event_dispatcher,
); );
@@ -418,14 +426,14 @@ impl BfdSession {
handle: BfdSessionHandle, handle: BfdSessionHandle,
control_sock: UdpSocket, control_sock: UdpSocket,
local_addr: IpAddr, local_addr: IpAddr,
remote_addr: IpAddr, peer: ResolvedPeer,
local_discr: BfdDiscriminator, local_discr: BfdDiscriminator,
event_dispatcher: EventDispatcherHandle, event_dispatcher: EventDispatcherHandle,
) -> Self { ) -> Self {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let periodic = let periodic =
BfdPeriodicSenderHandle::new(handle.clone(), time::Duration::from_secs(1).into()); 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 = let slow_desired_min_tx =
std::cmp::max(configured_desired_min_tx, BfdInterval::from_secs(1)); std::cmp::max(configured_desired_min_tx, BfdInterval::from_secs(1));
@@ -446,18 +454,18 @@ impl BfdSession {
state: BfdSessionState { state: BfdSessionState {
control_sock: Arc::new(control_sock), control_sock: Arc::new(control_sock),
local_ip: local_addr, local_ip: local_addr,
peer_addr: remote_addr, peer_addr: peer.address,
session_state: Atomic::new(BfdState::default()), session_state: Atomic::new(BfdState::default()),
remote_session_state: Atomic::new(BfdState::default()), remote_session_state: Atomic::new(BfdState::default()),
local_discr, local_discr,
remote_discr: Atomic::new(BfdDiscriminator(0)), remote_discr: Atomic::new(BfdDiscriminator(0)),
local_diag: Atomic::new(BfdDiagnostic::None), local_diag: Atomic::new(BfdDiagnostic::None),
desired_min_tx_interval: Atomic::new(slow_desired_min_tx), 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)), remote_min_rx_interval: Atomic::new(BfdInterval::from_millis(300)),
demand_mode: Atomic::new(false), demand_mode: Atomic::new(false),
remote_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), auth_type: Atomic::new(BfdAuthType::None),
rcv_auth_seq: Atomic::new(0), rcv_auth_seq: Atomic::new(0),
xmit_auth_seq: Atomic::new(rng.gen()), xmit_auth_seq: Atomic::new(rng.gen()),
@@ -975,7 +983,7 @@ impl EventDispatcherHandle {
struct Bfdd { struct Bfdd {
local: SocketAddr, local: SocketAddr,
peers: Vec<SocketAddr>, peers: Vec<ResolvedPeer>,
sessions_by_ip: HashMap<(IpAddr, IpAddr), BfdDiscriminator>, sessions_by_ip: HashMap<(IpAddr, IpAddr), BfdDiscriminator>,
sessions_by_discr: HashMap<BfdDiscriminator, BfdSessionHandle>, sessions_by_discr: HashMap<BfdDiscriminator, BfdSessionHandle>,
@@ -985,24 +993,18 @@ struct Bfdd {
} }
impl Bfdd { impl Bfdd {
async fn new(args: Args) -> Result<Self, Box<dyn Error>> { async fn new(config: ResolvedConfig) -> Result<Self, Box<dyn Error>> {
let local = SocketAddr::new(args.bind, CONTROL_PORT); let local = match config.bind.as_slice() {
if let Some(peer) = args [addr] => SocketAddr::new(*addr, CONTROL_PORT),
.peers addrs => {
.iter()
.find(|p| p.is_ipv4() != args.bind.is_ipv4())
{
return Err(format!( return Err(format!(
"peer {peer} is not the same address family as bind address {}", "{} bind addresses configured, but only one is currently supported",
args.bind addrs.len()
) )
.into()); .into())
} }
let peers = args };
.peers let peers = config.peers;
.into_iter()
.map(|ip| SocketAddr::new(ip, CONTROL_PORT))
.collect();
let sessions_by_ip = HashMap::new(); let sessions_by_ip = HashMap::new();
let sessions_by_discr = HashMap::new(); let sessions_by_discr = HashMap::new();
@@ -1016,7 +1018,7 @@ impl Bfdd {
let event_dispatcher = EventDispatcherHandle::new().await; let event_dispatcher = EventDispatcherHandle::new().await;
event_dispatcher event_dispatcher
.add_sink(Box::new(ScriptHookSink::new())) .add_sink(Box::new(ScriptHookSink::with_script_dir(config.hooks_dir)))
.await; .await;
Ok(Self { Ok(Self {
@@ -1037,14 +1039,14 @@ impl Bfdd {
} }
let session = BfdSessionHandle::new( let session = BfdSessionHandle::new(
self.local.ip(), self.local.ip(),
peer.ip(), *peer,
local_discr, local_discr,
self.event_dispatcher.clone(), self.event_dispatcher.clone(),
) )
.await?; .await?;
self.sessions_by_ip 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); self.sessions_by_discr.insert(local_discr, session);
} }
Ok(()) Ok(())
@@ -1156,6 +1158,13 @@ impl Bfdd {
} }
} }
fn load_config(args: Args) -> Result<ResolvedConfig, ConfigError> {
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] #[tokio::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
let args = Args::parse(); let args = Args::parse();
@@ -1164,8 +1173,36 @@ async fn main() -> io::Result<()> {
.format_timestamp_micros() .format_timestamp_micros()
.init(); .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, Ok(bfdd) => bfdd.run().await,
Err(e) => Err(std::io::Error::other(e.to_string())), 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);
}
}