When a session advertised a full table to a route collector at two thousand routes a second, nothing on the status page said whether the daemon or the peer set that pace. The answer is in the kernel: the peer's receive window, the queues, and how long the connection was held up by the window or by the send buffer, which `ss -ti` shows. This change asks the kernel the same way `ss` does, over the socket diagnostics netlink family (`NETLINK_SOCK_DIAG`, `SOCK_DIAG_BY_FAMILY` with the connection's addresses and ports), and reads `tcp_info` and the congestion control name from the answer. No external program is run and no unsafe code is needed: the request and the answer are byte layouts of the kernel's ABI, read field by field, with fields an older kernel does not send left at zero. A socket on a link-local address is found by its interface, which the request names. * `bgp_fib::sockdiag`: the request, the parser, `TcpConnection`, and `tcp_connection(local, peer)`; `Ok(None)` where there is no such connection or the system is not Linux. * `GET /api/tcp?peer=ADDRESS` in the HTTP API: the connection of the session with that peer, with times in milliseconds, the two windows named by what they allow, and the held-up times also as shares of the busy time. * The status page: a "tcp" link on each established session, which expands a row below it with the figures, re-asked on every refresh while open. The group rows' span is corrected for the "sent" column. Also on the status page: the attributes of a received route that have no column of their own were badges with their value in a tooltip, which is hard to read. Clicking the attrs cell now expands a row under the route with every attribute by its name (ORIGIN, AS_PATH, NEXT_HOP, MED, LOCAL_PREF, COMMUNITIES, LARGE_COMMUNITIES, AGGREGATOR and the rest), the path identifier, the selection rank and the RPKI and ASPA states; the badges stay. Both expansions use the terms of `ss` and the RFCs rather than sentences. Documentation: the endpoint in the HTTP API reference, and the link and the expansion in the monitoring guide. Testing: unit tests of the request and of the parser on a reply built field by field, and of a "no such socket" and a refused answer; a Linux-only test that asks the kernel about a loopback connection of the test's own and about one that does not exist; the lab's basics phase checks the endpoint for the FRR session; the page was checked by hand against the daemon in the Linux VM. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
332 lines
12 KiB
Rust
332 lines
12 KiB
Rust
//! Putting the best routes into the kernel's routing table, so that the
|
|
//! machine forwards packets by them (Linux, over rtnetlink).
|
|
//!
|
|
//! The routing table chooses a best route for each prefix. This crate
|
|
//! follows those choices and keeps the kernel's table the same: one route
|
|
//! for each prefix, to the best route's next hop. It also learns from the
|
|
//! kernel what interfaces and addresses the machine has, which is what
|
|
//! says whether a next hop can be reached at all.
|
|
//!
|
|
//! - [`netlink`] is the kernel's messages, as bytes and back.
|
|
//! - [`interfaces`] is the machine's interfaces and what is reachable on
|
|
//! them; [`routes`] the kernel's own routes, which say the way to what
|
|
//! is not; [`topology`] the two together with the kernel's nexthop
|
|
//! objects, as one view.
|
|
//! - [`plan`] works out what to ask the kernel for.
|
|
//! - The rest, which only Linux has, asks it.
|
|
//!
|
|
//! # What is and is not touched
|
|
//!
|
|
//! Our routes are marked as BGP's ([`netlink::PROTOCOL_BGP`]) and carry
|
|
//! the configured metric, in the configured table. Only routes with that
|
|
//! mark, metric and table are ever replaced or removed; and all such
|
|
//! routes are taken to be ours, so two daemons must not share them.
|
|
//! Routes left behind by a run that ended badly are found at the next
|
|
//! start, and removed unless they are still wanted. A run that ends in
|
|
//! good order removes its routes.
|
|
//!
|
|
//! # What is installed
|
|
//!
|
|
//! A best route whose next hop can be reached: a neighbor, which is an
|
|
//! address in the network of one of the machine's interfaces, or an
|
|
//! address the kernel's own routes know the way to. In the second case
|
|
//! the route goes into the kernel with the gateway and interface of the
|
|
//! route that knows the way, since the kernel does not look a gateway up
|
|
//! in turn: a route whose next hop is a router of our own AS, reached over
|
|
//! a tunnel by a static route, is installed as a route into the tunnel.
|
|
//! Which of the kernel's routes count for this, and which never do, is the
|
|
//! business of [`routes`].
|
|
|
|
pub mod interfaces;
|
|
#[cfg(target_os = "linux")]
|
|
mod kernel;
|
|
pub mod netlink;
|
|
pub mod plan;
|
|
pub mod routes;
|
|
pub mod sockdiag;
|
|
pub mod topology;
|
|
|
|
use std::net::IpAddr;
|
|
use std::sync::{Arc, Mutex, PoisonError};
|
|
use std::time::Duration;
|
|
|
|
use bgp_rib::table::RibTable;
|
|
use tokio::sync::watch;
|
|
use tokio::task::JoinHandle;
|
|
|
|
/// The metric our routes get unless configured otherwise: the one other
|
|
/// routing daemons give routes from external BGP peers, so that ours rank
|
|
/// against static and connected routes as an operator expects.
|
|
pub const DEFAULT_METRIC: u32 = 20;
|
|
|
|
/// How long routes left by an earlier run are kept unless configured
|
|
/// otherwise: time for sessions to come up and tables to be sent.
|
|
pub const DEFAULT_STALE_TIME: Duration = Duration::from_secs(120);
|
|
|
|
/// Where our routes go in the kernel.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct FibConfig {
|
|
/// The routing table: [`netlink::TABLE_MAIN`] to forward by them.
|
|
pub table: u32,
|
|
/// The metric, which with the table says which routes are ours.
|
|
pub metric: u32,
|
|
/// Have routes name nexthop objects where the kernel has them (Linux
|
|
/// 5.3 and later), which makes installing many routes a good deal
|
|
/// faster. Without, every route carries its own gateway.
|
|
pub nexthop_objects: bool,
|
|
/// How long routes of ours that an earlier run left in the kernel are
|
|
/// kept after a start without the routing table having them. The
|
|
/// table is empty until the peers have sent theirs again; removing
|
|
/// everything at once would stop the machine forwarding meanwhile.
|
|
/// After this long, what has not been announced again goes.
|
|
pub stale_time: Duration,
|
|
/// The kernel's table whose routes say the way to a next hop that is
|
|
/// not a neighbor: normally `main`, with the connected and static
|
|
/// routes. BGP's own routes in it never count; see [`routes`].
|
|
pub resolution_table: u32,
|
|
/// Whether the default route counts as the way to a next hop.
|
|
pub resolve_via_default: bool,
|
|
}
|
|
|
|
impl Default for FibConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
table: netlink::TABLE_MAIN,
|
|
metric: DEFAULT_METRIC,
|
|
nexthop_objects: true,
|
|
stale_time: DEFAULT_STALE_TIME,
|
|
resolution_table: netlink::TABLE_MAIN,
|
|
resolve_via_default: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Why routes cannot be kept in the kernel.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum FibError {
|
|
#[error("installing routes in the kernel is only supported on Linux")]
|
|
Unsupported,
|
|
#[error("the kernel's routing socket failed: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
#[error("the kernel sent something that is not netlink: {0}")]
|
|
Malformed(#[from] netlink::NetlinkError),
|
|
#[error("the kernel refused to list {what}: error {errno}")]
|
|
Dump { what: &'static str, errno: i32 },
|
|
#[error(
|
|
"not permitted to change the kernel's routes: the daemon needs the CAP_NET_ADMIN \
|
|
capability (or root)"
|
|
)]
|
|
NotPermitted,
|
|
}
|
|
|
|
/// One of the machine's interfaces, for showing.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct InterfaceStatus {
|
|
pub index: u32,
|
|
pub name: String,
|
|
pub up: bool,
|
|
/// Its addresses, each with the length of the network it is in.
|
|
pub addresses: Vec<(IpAddr, u8)>,
|
|
}
|
|
|
|
/// How things stand with the kernel, for showing.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct Status {
|
|
/// Whether the kernel has been brought in step with the routing table
|
|
/// at least once since the start.
|
|
pub synced: bool,
|
|
/// How many routes of ours the kernel has.
|
|
pub installed: usize,
|
|
/// How many of them are kept from an earlier run though the routing
|
|
/// table does not have them (yet); see [`FibConfig::stale_time`].
|
|
pub kept: usize,
|
|
/// How many of them are blackhole routes, which drop the packets for
|
|
/// their prefix.
|
|
pub blackholes: usize,
|
|
/// Prefixes whose best route is not in the kernel because its next
|
|
/// hop is not usable, by why.
|
|
pub unresolved: Vec<(&'static str, usize)>,
|
|
/// Prefixes whose best route the kernel refused.
|
|
pub refused: usize,
|
|
/// How many requests the kernel has refused in all.
|
|
pub errors: u64,
|
|
/// The last such refusal.
|
|
pub last_error: Option<String>,
|
|
pub interfaces: Vec<InterfaceStatus>,
|
|
}
|
|
|
|
/// The machine's interfaces, routes and nexthop objects as the kernel
|
|
/// last described them, as far as next hops are concerned; `None` until
|
|
/// it has. Whoever holds one is told when they change.
|
|
pub type TopologyWatch = watch::Receiver<Option<Arc<topology::Topology>>>;
|
|
|
|
/// How the task that keeps the kernel's routes is to end.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum Stop {
|
|
/// Taking our routes out of the kernel.
|
|
Withdraw,
|
|
/// Leaving them there, for the next run to find.
|
|
Keep,
|
|
}
|
|
|
|
struct Shared {
|
|
config: FibConfig,
|
|
status: Mutex<Status>,
|
|
topology: watch::Sender<Option<Arc<topology::Topology>>>,
|
|
stop: watch::Sender<Option<Stop>>,
|
|
/// How many routes of an earlier run were found in the kernel at the
|
|
/// start, once that has been looked at.
|
|
adopted: watch::Sender<Option<usize>>,
|
|
/// Set to have what is kept of an earlier run go now.
|
|
release: watch::Sender<bool>,
|
|
/// Whether the task has ended.
|
|
ended: watch::Sender<bool>,
|
|
}
|
|
|
|
/// Handle to the task that keeps the kernel's routes. Cheap to clone.
|
|
#[derive(Clone)]
|
|
pub struct Fib {
|
|
shared: Arc<Shared>,
|
|
}
|
|
|
|
impl Fib {
|
|
/// Starts keeping the kernel's routes in step with the best routes of
|
|
/// `rib`, and returns a handle with the task that does it. The task
|
|
/// ends only by failing, or when [`Fib::stop`] or [`Fib::leave`] is
|
|
/// called.
|
|
///
|
|
/// Must be called from within a Tokio runtime. Fails at once where
|
|
/// there is no kernel to talk to in this way.
|
|
pub fn spawn(
|
|
rib: Arc<RibTable>,
|
|
config: FibConfig,
|
|
) -> Result<(Self, JoinHandle<Result<(), FibError>>), FibError> {
|
|
let (stop, stopping) = watch::channel(None);
|
|
let fib = Self {
|
|
shared: Arc::new(Shared {
|
|
config,
|
|
status: Mutex::default(),
|
|
topology: watch::channel(None).0,
|
|
stop,
|
|
adopted: watch::channel(None).0,
|
|
release: watch::channel(false).0,
|
|
ended: watch::channel(false).0,
|
|
}),
|
|
};
|
|
let task = start(rib, fib.clone(), stopping)?;
|
|
Ok((fib, task))
|
|
}
|
|
|
|
pub fn config(&self) -> FibConfig {
|
|
self.shared.config
|
|
}
|
|
|
|
/// The kernel's view of the machine, for those who want to know
|
|
/// whether a next hop can be reached before the kernel is asked to use
|
|
/// it.
|
|
pub fn topology(&self) -> TopologyWatch {
|
|
self.shared.topology.subscribe()
|
|
}
|
|
|
|
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
|
fn publish_topology(&self, topology: &topology::Topology) {
|
|
self.shared
|
|
.topology
|
|
.send_replace(Some(Arc::new(topology.clone())));
|
|
}
|
|
|
|
pub fn status(&self) -> Status {
|
|
self.shared
|
|
.status
|
|
.lock()
|
|
.unwrap_or_else(PoisonError::into_inner)
|
|
.clone()
|
|
}
|
|
|
|
/// Has the task take our routes out of the kernel and end, and waits
|
|
/// until it has. Returns at once if it has ended already, for whatever
|
|
/// reason; the task's own result says how.
|
|
pub async fn stop(&self) {
|
|
self.end(Stop::Withdraw).await;
|
|
}
|
|
|
|
/// Has the task stop following the routing table and end, leaving our
|
|
/// routes in the kernel for the next run to find and keep for a while
|
|
/// (see [`FibConfig::stale_time`]), and waits until it has. For a
|
|
/// restart that is to go unnoticed by the packets being forwarded.
|
|
pub async fn leave(&self) {
|
|
self.end(Stop::Keep).await;
|
|
}
|
|
|
|
async fn end(&self, how: Stop) {
|
|
let mut ended = self.shared.ended.subscribe();
|
|
self.shared.stop.send_if_modified(|stop| {
|
|
let first = stop.is_none();
|
|
if first {
|
|
*stop = Some(how);
|
|
}
|
|
first
|
|
});
|
|
// Fails only if the handle that holds the sender is gone, and this
|
|
// is one.
|
|
let _ = ended.wait_for(|ended| *ended).await;
|
|
}
|
|
|
|
/// How many routes of an earlier run the kernel had at the start, once
|
|
/// the task has looked; zero if it ended before it did.
|
|
pub async fn adopted(&self) -> usize {
|
|
let mut adopted = self.shared.adopted.subscribe();
|
|
let mut ended = self.shared.ended.subscribe();
|
|
tokio::select! {
|
|
found = adopted.wait_for(Option::is_some) => {
|
|
found.ok().and_then(|found| *found).unwrap_or(0)
|
|
}
|
|
_ = ended.wait_for(|ended| *ended) => 0,
|
|
}
|
|
}
|
|
|
|
/// Has the routes of an earlier run that the routing table does not
|
|
/// have go now, rather than when [`FibConfig::stale_time`] is up: the
|
|
/// peers have all sent their routes, and what is not among them is no
|
|
/// longer anybody's.
|
|
pub fn release_kept(&self) {
|
|
self.shared.release.send_replace(true);
|
|
}
|
|
|
|
/// Changes the status, and gives back what `change` does.
|
|
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
|
fn update<R>(&self, change: impl FnOnce(&mut Status) -> R) -> R {
|
|
change(
|
|
&mut self
|
|
.shared
|
|
.status
|
|
.lock()
|
|
.unwrap_or_else(PoisonError::into_inner),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn start(
|
|
rib: Arc<RibTable>,
|
|
fib: Fib,
|
|
stopping: watch::Receiver<Option<Stop>>,
|
|
) -> Result<JoinHandle<Result<(), FibError>>, FibError> {
|
|
// Opened here, so that a machine that cannot is known of at once.
|
|
let sockets = kernel::Sockets::open()?;
|
|
Ok(tokio::spawn(async move {
|
|
let result = kernel::run(rib, fib.clone(), sockets, stopping).await;
|
|
fib.shared.ended.send_replace(true);
|
|
result
|
|
}))
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
fn start(
|
|
_rib: Arc<RibTable>,
|
|
_fib: Fib,
|
|
_stopping: watch::Receiver<Option<Stop>>,
|
|
) -> Result<JoinHandle<Result<(), FibError>>, FibError> {
|
|
Err(FibError::Unsupported)
|
|
}
|