Hygiene: scripts, CI, docs and manifests

- scripts/in-namespace.sh set CARGO_TARGET_DIR to the empty string when
  it was unset, which cargo refuses, and dropped RUSTFLAGS, so the run
  in the namespace rebuilt what kernel-tests.sh had just built. Each is
  passed on only where set.
- The README's list of unsupported features still had route reflection
  and graceful restart, both of which exist; the command-line reference
  lacked SIGUSR1 and exit status 75; the HTTP API reference did not say
  what errors look like.
- The event stream said "missed 0 events" for the best-routes stream,
  which does not count what it missed; it leaves the count out now.
- CI lints bgp-bench with its count-memory feature, which nothing built.
- bgp-bench had two unused dev-dependencies; bgp-integration-tests had
  three dependencies that only its tests and examples use.
- install.sh updates apt before installing; two-routers.sh loses an
  unused variable; small things in the web crate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-06 11:15:58 +02:00
1 parent e5341ea639
commit 175a13d506
18 files changed
+92 -88

No files matched your search

+4
View File
@@ -24,6 +24,10 @@ jobs:
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
- name: Lint - name: Lint
run: cargo clippy --locked --workspace --all-targets -- -D warnings run: cargo clippy --locked --workspace --all-targets -- -D warnings
# The memory-counting allocator of the benchmarks is behind a feature
# that the workspace build does not turn on.
- name: Lint the benchmarks with memory counting
run: cargo clippy --locked -p bgp-bench --all-targets --features count-memory -- -D warnings
- name: Test - name: Test
run: cargo test --locked --workspace run: cargo test --locked --workspace
# A link in the documentation to something that is not there. # A link in the documentation to something that is not there.
Generated
-2
View File
@@ -183,8 +183,6 @@ dependencies = [
"flate2", "flate2",
"stats_alloc", "stats_alloc",
"tokio", "tokio",
"tracing",
"tracing-subscriber",
] ]
[[package]] [[package]]
+12 -6
View File
@@ -12,9 +12,9 @@ Ferrix has not been deployed in a production network.
### Unsupported Features ### Unsupported Features
- Route reflection, per-client best path selection for route servers, and ADD-PATH for advertised routes - Per-client best path selection for route servers, and ADD-PATH for advertised routes
- TCP-AO, TCP MD5, and GTSM - TCP-AO, TCP MD5, and GTSM
- Graceful restart, BFD, and configuration changes without a restart - Long-Lived Graceful Restart, BFD, and configuration changes without a restart
- Recursive resolution of next hops that are not directly connected - Recursive resolution of next hops that are not directly connected
### Layout ### Layout
@@ -28,6 +28,8 @@ Ferrix has not been deployed in a production network.
| `crates/fib` | The best routes in the Linux kernel, over netlink | | `crates/fib` | The best routes in the Linux kernel, over netlink |
| `crates/server` | Sessions, the state machine, policy, configuration | | `crates/server` | Sessions, the state machine, policy, configuration |
| `crates/web` | The status page, HTTP API and metrics | | `crates/web` | The status page, HTTP API and metrics |
| `crates/integration-tests` | A test server and test peers, and the tests that run the daemon end to end |
| `crates/bench` | Benchmarks at the scale of the Internet routing table |
| `bin/bgp` | The daemon | | `bin/bgp` | The daemon |
## Documentation ## Documentation
@@ -39,7 +41,8 @@ The documentation is a website built from [`docs/`](docs/) with
zola --root docs serve # http://127.0.0.1:1111 zola --root docs serve # http://127.0.0.1:1111
``` ```
- [Overview](docs/content/docs/getting-started/introduction.md) and - [Overview](docs/content/docs/getting-started/introduction.md),
[installation](docs/content/docs/getting-started/installation.md) and
[quick start](docs/content/docs/getting-started/quick-start.md) [quick start](docs/content/docs/getting-started/quick-start.md)
- User guide: [BGP neighbors](docs/content/docs/guides/peers.md), - User guide: [BGP neighbors](docs/content/docs/guides/peers.md),
[import policy](docs/content/docs/guides/import-policy.md), [import policy](docs/content/docs/guides/import-policy.md),
@@ -47,15 +50,18 @@ zola --root docs serve # http://127.0.0.1:1111
[RPKI](docs/content/docs/guides/rpki.md), [RPKI](docs/content/docs/guides/rpki.md),
[kernel route installation](docs/content/docs/guides/kernel.md), [kernel route installation](docs/content/docs/guides/kernel.md),
[route advertisement](docs/content/docs/guides/passing-routes-on.md), [route advertisement](docs/content/docs/guides/passing-routes-on.md),
[configuration examples](docs/examples/) [graceful restart](docs/content/docs/guides/graceful-restart.md),
[configuration examples](docs/content/docs/guides/examples.md)
- [Deployment](docs/content/docs/operations/deployment.md) and - [Deployment](docs/content/docs/operations/deployment.md) and
[monitoring](docs/content/docs/operations/monitoring.md) [monitoring](docs/content/docs/operations/monitoring.md)
- Reference: [configuration](docs/content/docs/reference/configuration.md), - Reference: [configuration](docs/content/docs/reference/configuration.md),
[command line](docs/content/docs/reference/command-line.md),
[HTTP API](docs/content/docs/reference/http-api.md), [HTTP API](docs/content/docs/reference/http-api.md),
[metrics](docs/content/docs/reference/metrics.md), [metrics](docs/content/docs/reference/metrics.md),
[supported standards](docs/content/docs/reference/rfcs.md) [supported standards](docs/content/docs/reference/rfcs.md)
- [Architecture](docs/content/docs/development/architecture.md) and - [Architecture](docs/content/docs/development/architecture.md),
[testing](docs/content/docs/development/testing.md) [testing](docs/content/docs/development/testing.md) and
[contributing](docs/content/docs/development/contributing.md)
## Development ## Development
-2
View File
@@ -21,8 +21,6 @@ bgp-integration-tests.workspace = true
bgp-rib.workspace = true bgp-rib.workspace = true
bgp-server.workspace = true bgp-server.workspace = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
[features] [features]
# Counts every allocation, to report how much memory a table holds. The # Counts every allocation, to report how much memory a table holds. The
+1 -8
View File
@@ -17,7 +17,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use bgp_bench::memory::{Region, describe}; use bgp_bench::memory::{Region, describe};
use bgp_bench::{Prefixes, Shape, announcements, encoded, wire_context}; use bgp_bench::{Prefixes, Shape, announcements, encoded, env, wire_context};
use bgp_integration_tests::test_peer::TestPeer; use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer; use bgp_integration_tests::test_server::TestServer;
use bgp_packet::message::{Message, PathAttribute}; use bgp_packet::message::{Message, PathAttribute};
@@ -25,13 +25,6 @@ use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::table::{PeerIndex, PeerInfo, RibTable}; use bgp_rib::table::{PeerIndex, PeerInfo, RibTable};
use bytes::BytesMut; use bytes::BytesMut;
fn env(name: &str, default: usize) -> usize {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn mib(bytes: usize) -> f64 { fn mib(bytes: usize) -> f64 {
bytes as f64 / (1024.0 * 1024.0) bytes as f64 / (1024.0 * 1024.0)
} }
+1 -8
View File
@@ -7,7 +7,7 @@ use std::net::Ipv4Addr;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use bgp_bench::{Prefixes, Shape, announcements, encoded}; use bgp_bench::{Prefixes, Shape, announcements, encoded, env};
use bgp_integration_tests::test_peer::TestPeer; use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer; use bgp_integration_tests::test_server::TestServer;
use bgp_packet::message::PathAttribute; use bgp_packet::message::PathAttribute;
@@ -15,13 +15,6 @@ use bgp_server::marks::{Control, Marks};
use bgp_server::policy::ExportPolicy; use bgp_server::policy::ExportPolicy;
use bytes::BytesMut; use bytes::BytesMut;
fn env(name: &str, default: usize) -> usize {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
/// How a server passes routes on in one run. /// How a server passes routes on in one run.
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum Passing { enum Passing {
+15 -5
View File
@@ -1,9 +1,10 @@
//! A synthetic copy of the Internet routing table, as the UPDATE messages a //! A synthetic copy of the Internet routing table, as the UPDATE messages a
//! peer would send to announce it. //! peer would send to announce it.
//! //!
//! It is made up, not captured, so that the benchmarks need no data files //! It is made up, not captured, so that the benchmarks of a synthetic table
//! and give the same input every run. It is shaped like the real thing where //! need no data files and give the same input every run; the one of a real
//! that matters for cost: //! table reads an MRT dump instead, see [`mrt`]. It is shaped like the real
//! thing where that matters for cost:
//! //!
//! - about a million IPv4 prefixes, most of them /24s, and about a quarter //! - about a million IPv4 prefixes, most of them /24s, and about a quarter
//! of a million IPv6 prefixes, most of them /48s; //! of a million IPv6 prefixes, most of them /48s;
@@ -163,7 +164,7 @@ pub fn announcements(prefixes: &Prefixes, shape: Shape, peer: u32) -> Vec<Update
let hops = 2 + rng.below(4) as usize; let hops = 2 + rng.below(4) as usize;
let mut path = vec![peer_asn]; let mut path = vec![peer_asn];
path.extend((0..hops).map(|_| 1 + rng.below(400_000) as u32)); path.extend((0..hops).map(|_| 1 + rng.below(400_000) as u32));
let mut attributes = vec![ let attributes = vec![
PathAttribute::Origin(OriginPathAttribute::IGP), PathAttribute::Origin(OriginPathAttribute::IGP),
PathAttribute::AsPath(AsPathAttribute { PathAttribute::AsPath(AsPathAttribute {
segments: vec![AsPathSegment { segments: vec![AsPathSegment {
@@ -199,7 +200,7 @@ pub fn announcements(prefixes: &Prefixes, shape: Shape, peer: u32) -> Vec<Update
} else { } else {
None None
}; };
(std::mem::take(&mut attributes), later, large) (attributes, later, large)
}; };
let mut updates = Vec::new(); let mut updates = Vec::new();
@@ -283,3 +284,12 @@ pub fn wire_context() -> ParserContext {
add_path: Vec::new(), add_path: Vec::new(),
} }
} }
/// The value of the environment variable `name` as a number, or `default`
/// when it is not set or is not one: how the benchmarks take their size.
pub fn env(name: &str, default: usize) -> usize {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
+5 -2
View File
@@ -17,11 +17,14 @@ bgp-rpki.workspace = true
bgp-server.workspace = true bgp-server.workspace = true
bgp-web.workspace = true bgp-web.workspace = true
bytes.workspace = true bytes.workspace = true
clap.workspace = true
eyre.workspace = true eyre.workspace = true
tokio.workspace = true
# Used by the examples and the tests only.
[dev-dependencies]
clap.workspace = true
reqwest.workspace = true reqwest.workspace = true
serde_json.workspace = true serde_json.workspace = true
tokio.workspace = true
[lints] [lints]
workspace = true workspace = true
+15 -31
View File
@@ -1,12 +1,11 @@
//! JSON handlers and their view types. Everything here is read-only: the //! JSON handlers and their view types. Everything here is read-only: the
//! daemon is configured through its configuration file. //! daemon is configured through its configuration file.
use std::collections::{BTreeMap, BinaryHeap, HashMap}; use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet};
use std::net::{IpAddr, Ipv4Addr}; use std::net::{IpAddr, Ipv4Addr};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use axum::Json; use axum::Json;
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
@@ -210,7 +209,7 @@ impl PeerView {
pub fn peer_views(registry: &SessionRegistry, restarts: &Restarts) -> Vec<PeerView> { pub fn peer_views(registry: &SessionRegistry, restarts: &Restarts) -> Vec<PeerView> {
let kept = restarts.kept(); let kept = restarts.kept();
let sessions = registry.list(); let sessions = registry.list();
let mut claimed = std::collections::HashSet::new(); let mut claimed = HashSet::new();
let mut views: Vec<PeerView> = registry let mut views: Vec<PeerView> = registry
.configured_peers() .configured_peers()
.iter() .iter()
@@ -698,15 +697,16 @@ enum Scope {
Within(IpPrefix), Within(IpPrefix),
} }
/// A prefix given in a query.
fn parse_prefix(text: &str) -> Result<IpPrefix, QueryError> {
IpPrefix::try_from(text).map_err(|e| QueryError::Bad(format!("{text:?} is not a prefix: {e}")))
}
impl Scope { impl Scope {
fn of(query: &RouteQuery) -> Result<Self, QueryError> { fn of(query: &RouteQuery) -> Result<Self, QueryError> {
let prefix = |text: &str| {
IpPrefix::try_from(text)
.map_err(|e| QueryError::Bad(format!("{text:?} is not a prefix: {e}")))
};
match (&query.prefix, &query.address, &query.within) { match (&query.prefix, &query.address, &query.within) {
(None, None, None) => Ok(Self::All), (None, None, None) => Ok(Self::All),
(Some(text), None, None) => Ok(Self::Exactly(prefix(text)?)), (Some(text), None, None) => Ok(Self::Exactly(parse_prefix(text)?)),
(None, Some(text), None) => { (None, Some(text), None) => {
let address: IpAddr = text let address: IpAddr = text
.parse() .parse()
@@ -719,7 +719,7 @@ impl Scope {
host.map(Self::Covering) host.map(Self::Covering)
.map_err(|e| QueryError::Bad(e.to_string())) .map_err(|e| QueryError::Bad(e.to_string()))
} }
(None, None, Some(text)) => Ok(Self::Within(prefix(text)?)), (None, None, Some(text)) => Ok(Self::Within(parse_prefix(text)?)),
_ => Err(QueryError::Bad( _ => Err(QueryError::Bad(
"give at most one of prefix, address and within".into(), "give at most one of prefix, address and within".into(),
)), )),
@@ -753,14 +753,10 @@ enum Place {
impl Place { impl Place {
fn of(query: &RouteQuery) -> Result<Self, QueryError> { fn of(query: &RouteQuery) -> Result<Self, QueryError> {
let prefix = |text: &str| {
IpPrefix::try_from(text)
.map_err(|e| QueryError::Bad(format!("{text:?} is not a prefix: {e}")))
};
match (&query.after, &query.before) { match (&query.after, &query.before) {
(None, None) => Ok(Self::Start), (None, None) => Ok(Self::Start),
(Some(after), None) => Ok(Self::After(prefix(after)?)), (Some(after), None) => Ok(Self::After(parse_prefix(after)?)),
(None, Some(before)) => Ok(Self::Before(prefix(before)?)), (None, Some(before)) => Ok(Self::Before(parse_prefix(before)?)),
(Some(_), Some(_)) => Err(QueryError::Bad( (Some(_), Some(_)) => Err(QueryError::Bad(
"give at most one of after and before".into(), "give at most one of after and before".into(),
)), )),
@@ -988,8 +984,8 @@ pub async fn received_routes(
} }
/// Validation with the RPKI: where the data comes from and how fresh it /// Validation with the RPKI: where the data comes from and how fresh it
/// is. /// is. The default is the view of a router that does not validate.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Default, Serialize)]
pub struct RpkiView { pub struct RpkiView {
/// Whether validation is configured at all. /// Whether validation is configured at all.
pub enabled: bool, pub enabled: bool,
@@ -1012,21 +1008,9 @@ pub struct RpkiView {
} }
pub async fn rpki(State(state): State<AppState>) -> Json<RpkiView> { pub async fn rpki(State(state): State<AppState>) -> Json<RpkiView> {
// Validation not configured: nothing to say, and `enabled` false.
let Some(status) = state.rpki.as_ref().map(|rpki| rpki.status()) else { let Some(status) = state.rpki.as_ref().map(|rpki| rpki.status()) else {
return Json(RpkiView { return Json(RpkiView::default());
enabled: false,
cache: None,
state: None,
session_id: None,
serial: None,
protocol_version: None,
vrps_ipv4: 0,
vrps_ipv6: 0,
aspas: 0,
updated_at_unix: None,
expires_at_unix: None,
last_error: None,
});
}; };
Json(RpkiView { Json(RpkiView {
enabled: true, enabled: true,
+7 -4
View File
@@ -52,7 +52,8 @@ pub enum WireEvent {
/// table. /// table.
Lagged { Lagged {
stream: &'static str, stream: &'static str,
missed: u64, #[serde(skip_serializing_if = "Option::is_none")]
missed: Option<u64>,
}, },
} }
@@ -71,7 +72,7 @@ impl WireEvent {
}, },
Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged { Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged {
stream: "sessions", stream: "sessions",
missed: n, missed: Some(n),
}, },
} }
} }
@@ -86,7 +87,7 @@ impl WireEvent {
}, },
Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged { Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged {
stream: "local_routes", stream: "local_routes",
missed: n, missed: Some(n),
}, },
} }
} }
@@ -153,9 +154,11 @@ fn best_routes(state: &AppState) -> impl Stream<Item = WireEvent> + use<> {
Change::Withdrawn(prefix) => WireEvent::BestWithdrawn { Change::Withdrawn(prefix) => WireEvent::BestWithdrawn {
prefix: prefix.to_string(), prefix: prefix.to_string(),
}, },
// The journal does not say how many changes went
// by before the reader was given the table again.
Change::Lagged => WireEvent::Lagged { Change::Lagged => WireEvent::Lagged {
stream: "best_routes", stream: "best_routes",
missed: 0, missed: None,
}, },
}; };
if events.send(event).await.is_err() { if events.send(event).await.is_err() {
+3 -7
View File
@@ -14,8 +14,7 @@ use std::sync::Arc;
use std::time::{Instant, SystemTime}; use std::time::{Instant, SystemTime};
use axum::Router; use axum::Router;
use axum::http::header; use axum::response::{Html, Redirect};
use axum::response::{Html, IntoResponse, Redirect};
use axum::routing::get; use axum::routing::get;
use bgp_fib::Fib; use bgp_fib::Fib;
use bgp_rib::LocalRib; use bgp_rib::LocalRib;
@@ -111,11 +110,8 @@ pub struct AppState {
/// The status page, embedded so the binary is self-contained. /// The status page, embedded so the binary is self-contained.
const STATUSZ_HTML: &str = include_str!("../static/statusz.html"); const STATUSZ_HTML: &str = include_str!("../static/statusz.html");
async fn statusz() -> impl IntoResponse { async fn statusz() -> Html<&'static str> {
( Html(STATUSZ_HTML)
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
Html(STATUSZ_HTML),
)
} }
/// Builds the router. Exposed so tests can drive it without a socket. /// Builds the router. Exposed so tests can drive it without a socket.
+3 -2
View File
@@ -26,10 +26,11 @@ pub async fn metricz(State(state): State<AppState>) -> impl IntoResponse {
RIB_JOURNAL_ENTRIES.set(gauge(state.rib.journal_len())); RIB_JOURNAL_ENTRIES.set(gauge(state.rib.journal_len()));
let mut buf = Vec::new(); let mut buf = Vec::new();
match TextEncoder::new().encode(&prometheus::gather(), &mut buf) { let encoder = TextEncoder::new();
match encoder.encode(&prometheus::gather(), &mut buf) {
Ok(()) => ( Ok(()) => (
StatusCode::OK, StatusCode::OK,
[(header::CONTENT_TYPE, "text/plain; version=0.0.4")], [(header::CONTENT_TYPE, encoder.format_type())],
buf, buf,
) )
.into_response(), .into_response(),
+2 -2
View File
@@ -495,7 +495,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
function describe(ev) { function describe(ev) {
switch (ev.type) { switch (ev.type) {
case 'session_up': return `<span class="ok">session UP</span> with ${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn} (opened by ${ev.peer.direction === 'outbound' ? 'us' : 'peer'})`; case 'session_up': return `<span class="ok">session UP</span> with ${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn} (opened by ${ev.peer.direction === 'outbound' ? 'us' : 'peer'})`;
case 'route_refresh_requested': return `${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn} asked for a route refresh; re-sent all originated routes`; case 'route_refresh_requested': return `${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn} asked for a route refresh; re-sent its routes`;
case 'session_down': return `<span class="bad">session DOWN</span> with ${esc(ev.peer_addr)} AS${ev.peer_asn}`; case 'session_down': return `<span class="bad">session DOWN</span> with ${esc(ev.peer_addr)} AS${ev.peer_asn}`;
case 'local_announced': { const r = ev.route; return `originating ${esc(r.prefix)}` + case 'local_announced': { const r = ev.route; return `originating ${esc(r.prefix)}` +
(r.communities.length ? ` [${names(r.communities)}]` : '') + (r.large_communities.length ? ` [${names(r.large_communities)}]` : '') + (r.communities.length ? ` [${names(r.communities)}]` : '') + (r.large_communities.length ? ` [${names(r.large_communities)}]` : '') +
@@ -503,7 +503,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
case 'local_withdrawn': return `<span class="warn">no longer originating</span> ${esc(ev.prefix)}`; case 'local_withdrawn': return `<span class="warn">no longer originating</span> ${esc(ev.prefix)}`;
case 'best_changed': return `best route for ${esc(ev.route.prefix)} is now from AS${ev.route.peer_asn} path [${ev.route.summary.as_path.join(' ')}]`; case 'best_changed': return `best route for ${esc(ev.route.prefix)} is now from AS${ev.route.peer_asn} path [${ev.route.summary.as_path.join(' ')}]`;
case 'best_withdrawn': return `${esc(ev.prefix)} has <span class="warn">no route</span> any more`; case 'best_withdrawn': return `${esc(ev.prefix)} has <span class="warn">no route</span> any more`;
case 'lagged': return `<span class="bad">missed ${ev.missed} events</span> on ${esc(ev.stream)}`; case 'lagged': return `<span class="bad">missed ${ev.missed == null ? '' : ev.missed + ' '}events</span> on ${esc(ev.stream)}`;
default: return esc(JSON.stringify(ev)); default: return esc(JSON.stringify(ev));
} }
} }
+4 -2
View File
@@ -32,8 +32,9 @@ validates the file.
| Status | Meaning | | Status | Meaning |
|---|---| |---|---|
| 0 | With `--check`: the configuration is valid. Otherwise: the daemon stopped in response to a signal. | | 0 | With `--check`: the configuration is valid. Otherwise: the daemon stopped in response to `SIGTERM` or `SIGINT`. |
| 1 | The configuration is invalid, or the daemon failed. The error is written to standard error. | | 1 | The configuration is invalid, or the daemon failed. The error is written to standard error. |
| 75 | The daemon exited in response to `SIGUSR1`, to be started again. The systemd unit restarts it on this status (`RestartForceExitStatus=75`). See [Upgrading Ferrix](@/docs/operations/deployment.md#upgrading-ferrix). |
## Environment Variables ## Environment Variables
@@ -45,7 +46,8 @@ validates the file.
| Signal | Action | | Signal | Action |
|---|---| |---|---|
| `SIGTERM`, `SIGINT` | Sends a Cease NOTIFICATION message with the subcode Administrative Shutdown to every neighbor, removes the daemon's routes from the kernel, and exits. | | `SIGTERM`, `SIGINT` | Sends a Cease NOTIFICATION message with the subcode Administrative Shutdown to every neighbor, removes the daemon's routes from the kernel, and exits with status 0. |
| `SIGUSR1` | Starts a graceful restart (RFC 4724): neighbors on which graceful restart is in use retain the routes of Ferrix for the restart time, other neighbors receive a Cease NOTIFICATION message with the subcode Administrative Shutdown, the routes stay in the kernel, and the daemon exits with status 75. See [Upgrading Ferrix](@/docs/operations/deployment.md#upgrading-ferrix). |
Ferrix does not reload its configuration while it runs. To apply a new Ferrix does not reload its configuration while it runs. To apply a new
configuration, restart the daemon. See configuration, restart the daemon. See
+11 -4
View File
@@ -8,9 +8,16 @@ nav = "HTTP API"
+++ +++
Ferrix serves its HTTP interface on the address configured with Ferrix serves its HTTP interface on the address configured with
`http_endpoint`. All endpoints are read-only and use the `GET` method. If a `http_endpoint`. All endpoints are read-only and use the `GET` method.
query parameter is invalid, the server returns status 400 (Bad Request)
with a description of the error. An error response has a `text/plain` body that describes the error. The
server returns the following error statuses:
| Status | Meaning |
|---|---|
| 400 (Bad Request) | A query parameter is invalid. |
| 500 (Internal Server Error) | The server could not produce the response, for example because the metrics could not be encoded. |
| 503 (Service Unavailable) | A table is unavailable because the daemon is shutting down. |
> [!CAUTION] > [!CAUTION]
> The HTTP interface does not require authentication. Bind it to a loopback > The HTTP interface does not require authentication. Bind it to a loopback
@@ -229,7 +236,7 @@ identifies the event, as listed in Table 2.
| `local_announced`, `local_withdrawn` | An originated route was advertised or withdrawn. | | `local_announced`, `local_withdrawn` | An originated route was advertised or withdrawn. |
| `best_changed` | The best path for a prefix changed. The `route` field contains the new best path. | | `best_changed` | The best path for a prefix changed. The `route` field contains the new best path. |
| `best_withdrawn` | A prefix no longer has a route. | | `best_withdrawn` | A prefix no longer has a route. |
| `lagged` | The client did not read events quickly enough and missed events on `stream`. Reload the corresponding table. | | `lagged` | The client did not read events quickly enough and missed events on `stream`. Reload the corresponding table. The `missed` field gives the number of missed events for the `sessions` and `local_routes` streams; the `best_routes` stream omits it because the number is not known. |
The following command displays the event stream: The following command displays the event stream:
+8 -2
View File
@@ -11,8 +11,14 @@ sudo modprobe dummy 2>/dev/null || true
sudo ip netns add "$namespace" sudo ip netns add "$namespace"
trap 'sudo ip netns del "$namespace"' EXIT trap 'sudo ip netns del "$namespace"' EXIT
sudo ip netns exec "$namespace" ip link set lo up sudo ip netns exec "$namespace" ip link set lo up
# Only the variables that are set are passed on: cargo refuses an empty
# CARGO_TARGET_DIR, and the build settings have to be the same as the
# prebuild's in scripts/kernel-tests.sh for it to be reused.
sudo ip netns exec "$namespace" \ sudo ip netns exec "$namespace" \
setpriv --reuid "$(id -u)" --regid "$(id -g)" --init-groups \ setpriv --reuid "$(id -u)" --regid "$(id -g)" --init-groups \
--inh-caps +net_admin --ambient-caps +net_admin \ --inh-caps +net_admin --ambient-caps +net_admin \
env PATH="$PATH" HOME="$HOME" CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-}" \ env PATH="$PATH" HOME="$HOME" \
FERRIX_ROUTES="${FERRIX_ROUTES:-}" "$@" ${CARGO_TARGET_DIR:+CARGO_TARGET_DIR="$CARGO_TARGET_DIR"} \
${RUSTFLAGS:+RUSTFLAGS="$RUSTFLAGS"} \
${CARGO_TERM_COLOR:+CARGO_TERM_COLOR="$CARGO_TERM_COLOR"} \
${FERRIX_ROUTES:+FERRIX_ROUTES="$FERRIX_ROUTES"} "$@"
+1
View File
@@ -3,6 +3,7 @@
# BIRD from the distribution, Routinator from crates.io (a few minutes to # BIRD from the distribution, Routinator from crates.io (a few minutes to
# build), and radvd, which tells FRR who is on a link. # build), and radvd, which tells FRR who is on a link.
set -eu set -eu
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y frr bird2 jq curl radvd sudo DEBIAN_FRONTEND=noninteractive apt-get install -y frr bird2 jq curl radvd
# They are run by the lab, in namespaces, not as services of this machine. # They are run by the lab, in namespaces, not as services of this machine.
sudo systemctl disable --now frr bird radvd 2>/dev/null || true sudo systemctl disable --now frr bird radvd 2>/dev/null || true
-1
View File
@@ -97,7 +97,6 @@ start() {
pids="$pids $!" pids="$pids $!"
} }
start "$a" a start "$a" a
pid_a=$!
start "$b" b start "$b" b
# Waits until `ip route` in a namespace shows, or no longer shows, a route. # Waits until `ip route` in a namespace shows, or no longer shows, a route.