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
+72 -35
+72 -35
View File
@@ -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<PathBuf>,
/// 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<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>,
}
@@ -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<Self> {
@@ -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<SocketAddr>,
peers: Vec<ResolvedPeer>,
sessions_by_ip: HashMap<(IpAddr, IpAddr), BfdDiscriminator>,
sessions_by_discr: HashMap<BfdDiscriminator, BfdSessionHandle>,
@@ -985,24 +993,18 @@ struct Bfdd {
}
impl Bfdd {
async fn new(args: Args) -> Result<Self, Box<dyn Error>> {
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<Self, Box<dyn Error>> {
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<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]
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);
}
}