policy, config, web: the TCP gauges named once, one revalidator, one peer-naming rule
ci / interop (push) Canceled after 0s
ci / check (push) Canceled after 0s
ci / docs (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s

A code-quality pass over policy, configuration and the status API.
The twenty TCP gauges were spelled out in the struct, the
constructor, the removal, the sampler in the web crate and the status
view; one macro names each once with its help text, `PeerTcp::set`
fills them from a connection, and the view is a `From`. Every metric
and the list that forces them at start come from one declaration,
with a test that every declared family is registered. The two
revalidators, for next hops and for RPKI, share the task body that
scans the table off the sessions' threads and fans the findings out
to every session; the RPKI scan judges a route with the same `Checks`
the import path uses. Which peer a query names is one rule for the
received, rejected and TCP endpoints, so `/api/tcp` takes a name or an
address like the others. The config parser says proto3's "zero is
unset" in three helpers instead of a dozen shapes, and `parse_peer`
is three named parts shorter. The policy's test-only next-hop flag is
named for what it does, `accept_own_host_next_hop`; the "listed, or
else ours" prefix rule and the mark-then-prefer step each exist once.
The status page derives its spanning rows from the table's header,
shares one key-value grid and one detail-row rule, and defines its
helpers before their use. The web crate no longer re-exports views
nothing uses, and the TCP endpoint sits above the test module.

Testing: the TCP view's arithmetic (a share of no busy time is zero,
not NaN), the peer-naming rule, the RPKI scan finding only what a
change can have made invalid, and `/api/tcp` by address, by name and
for an unknown peer. The server and web tests, the HTTP, roles, RPKI
and import-policy integration tests, the docs build and every lab
phase pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-10 22:44:32 +02:00
1 parent 4cddd6fb04
commit db8edaae8e
13 files changed
+1067 -909

No files matched your search

@@ -52,6 +52,27 @@ async fn test_status_and_peers() -> Result<()> {
peers[0]["address_families"], peers[0]["address_families"],
serde_json::json!(["IPv4 unicast", "IPv6 unicast"]) serde_json::json!(["IPv4 unicast", "IPv6 unicast"])
); );
// The session's TCP connection, by the address the peer list gives.
// What the kernel says of it depends on the platform; that it was
// asked, and why it had nothing to say if it had not, does not.
let peer_addr = peers[0]["peer_addr"].as_str().unwrap();
let tcp = get_json(&client, &format!("{base}/api/tcp?peer={peer_addr}")).await?;
let available = tcp["available"].as_bool().expect("available is a bool");
if available {
assert!(tcp["connection"].is_object(), "{tcp}");
} else {
assert!(tcp["reason"].is_string(), "{tcp}");
}
// The address alone names the peer too, as it does elsewhere.
let by_address = get_json(&client, &format!("{base}/api/tcp?peer=127.0.0.1")).await?;
assert_eq!(by_address["available"], tcp["available"]);
// A peer there is no session with is a bad request, not an answer.
let nobody = client
.get(format!("{base}/api/tcp?peer=nobody"))
.send()
.await?;
assert_eq!(nobody.status(), 400);
Ok(()) Ok(())
} }
+14
View File
@@ -54,6 +54,14 @@ impl From<IpAddr> for PeerAddress {
} }
} }
/// Whether `wanted`, as someone typed it, names the peer at `address`
/// that is configured as `name`, if it is: by that name, or by the address
/// as [`PeerAddress::is_called`] reads it. The one rule for every place a
/// peer is asked for by name.
pub fn names_peer(wanted: &str, name: Option<&str>, address: PeerAddress) -> bool {
name == Some(wanted) || address.is_called(wanted)
}
/// The address, and the interface by name if there is one: `fe80::1%eth0`. /// The address, and the interface by name if there is one: `fe80::1%eth0`.
/// An interface that is no longer there goes by its number. /// An interface that is no longer there goes by its number.
impl fmt::Display for PeerAddress { impl fmt::Display for PeerAddress {
@@ -325,6 +333,12 @@ mod tests {
assert!(there.is_called("fe80::1")); assert!(there.is_called("fe80::1"));
assert!(!there.is_called("fe80::1%other")); assert!(!there.is_called("fe80::1%other"));
assert!(!there.is_called("fe80::2")); assert!(!there.is_called("fe80::2"));
// By its configured name as well, where it has one.
assert!(names_peer("upstream", Some("upstream"), there));
assert!(names_peer("fe80::1", Some("upstream"), there));
assert!(names_peer("fe80::1", None, there));
assert!(!names_peer("upstream", None, there));
assert!(!names_peer("other", Some("upstream"), there));
assert_eq!( assert_eq!(
socket_text(peer.to_dial().unwrap()), socket_text(peer.to_dial().unwrap()),
format!("[fe80::1%{name}]:179") format!("[fe80::1%{name}]:179")
+155 -128
View File
@@ -11,6 +11,7 @@ use bgp_primitives::constants::{AfiSafi, IpVersion};
use bgp_primitives::ip_prefix::IpPrefix; use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs}; use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_rpki::RpkiConfig; use bgp_rpki::RpkiConfig;
use protobuf::EnumOrUnknown;
use crate::address::{Endpoint, PeerAddress}; use crate::address::{Endpoint, PeerAddress};
use crate::error::{ConfigError, ExportProblem, ImportProblem, OriginateProblem, PeerProblem}; use crate::error::{ConfigError, ExportProblem, ImportProblem, OriginateProblem, PeerProblem};
@@ -141,20 +142,28 @@ impl Config {
let listen_endpoints = parse_listen_endpoints(&proto.listen_endpoints)?; let listen_endpoints = parse_listen_endpoints(&proto.listen_endpoints)?;
let defaults = PeerDefaults { let defaults = PeerDefaults {
export: Arc::new(match proto.export.as_ref() { export: Arc::new(
Some(export) => parse_export(export)?, proto
None => ExportPolicy::default(), .export
}), .as_ref()
export_limits: match proto.export_limits.as_ref() { .map(parse_export)
Some(limits) => parse_export_limits(limits) .transpose()?
.unwrap_or_default(),
),
export_limits: proto
.export_limits
.as_ref()
.map(parse_export_limits)
.transpose()
.map_err(ConfigError::ExportLimits)? .map_err(ConfigError::ExportLimits)?
.flatten()
.map(Arc::new), .map(Arc::new),
None => None, graceful_restart: proto
}, .graceful_restart
graceful_restart: match proto.graceful_restart.as_ref() { .as_ref()
Some(own) => GracefulRestartConfig::default().overridden_by(own)?, .map(|own| GracefulRestartConfig::default().overridden_by(own))
None => GracefulRestartConfig::default(), .transpose()?
}, .unwrap_or_default(),
}; };
let mut peers = parse_peers(proto.peers, proto.local_asn, &defaults)?; let mut peers = parse_peers(proto.peers, proto.local_asn, &defaults)?;
check_roles( check_roles(
@@ -179,16 +188,7 @@ impl Config {
None => None, None => None,
}; };
let rpki = match proto.rpki.as_ref() { let rpki = proto.rpki.as_ref().map(parse_rpki).transpose()?;
Some(rpki) => Some(parse_rpki(rpki)?),
None => None,
};
let connect_retry = if proto.connect_retry_seconds == 0 {
DEFAULT_CONNECT_RETRY
} else {
Duration::from_secs(proto.connect_retry_seconds as u64)
};
Ok(Self { Ok(Self {
router_id, router_id,
@@ -197,7 +197,7 @@ impl Config {
hold_time_secs: DEFAULT_HOLD_TIME_SECS, hold_time_secs: DEFAULT_HOLD_TIME_SECS,
listen_endpoints, listen_endpoints,
peers, peers,
connect_retry, connect_retry: seconds(proto.connect_retry_seconds).unwrap_or(DEFAULT_CONNECT_RETRY),
collision_detect_established_state: proto.collision_detect_established_state, collision_detect_established_state: proto.collision_detect_established_state,
accept_unconfigured_peers: proto.accept_unconfigured_peers, accept_unconfigured_peers: proto.accept_unconfigured_peers,
originate, originate,
@@ -398,20 +398,37 @@ fn parse_identifier(text: &str) -> Option<Ipv4Addr> {
.filter(|id| !id.is_unspecified()) .filter(|id| !id.is_unspecified())
} }
/// A number the configuration has as zero is a field left out: the
/// protocol buffer has no way of telling the two apart, and nothing
/// configured here is meaningfully zero.
fn given(n: u32) -> Option<u32> {
(n > 0).then_some(n)
}
/// A time given in seconds, if it was given.
fn seconds(n: u32) -> Option<Duration> {
given(n).map(|n| Duration::from_secs(n.into()))
}
/// A list of prefixes, if any are listed: an empty list leaves the setting
/// out rather than allowing nothing.
fn parse_prefixes_if_any(
ranges: &[proto::PrefixRange],
) -> Result<Option<PrefixSet>, ImportProblem> {
if ranges.is_empty() {
return Ok(None);
}
parse_prefixes(ranges).map(Some)
}
fn parse_kernel(kernel: &proto::Kernel) -> FibConfig { fn parse_kernel(kernel: &proto::Kernel) -> FibConfig {
let defaults = FibConfig::default(); let defaults = FibConfig::default();
FibConfig { FibConfig {
// The kernel has no table 0 to put routes in. // The kernel has no table 0 to put routes in.
table: match kernel.table { table: given(kernel.table).unwrap_or(defaults.table),
0 => defaults.table,
table => table,
},
metric: kernel.metric.unwrap_or(defaults.metric), metric: kernel.metric.unwrap_or(defaults.metric),
nexthop_objects: !kernel.without_nexthop_objects, nexthop_objects: !kernel.without_nexthop_objects,
resolution_table: match kernel.resolution_table { resolution_table: given(kernel.resolution_table).unwrap_or(defaults.resolution_table),
0 => defaults.resolution_table,
table => table,
},
resolve_via_default: kernel.resolve_via_default_route, resolve_via_default: kernel.resolve_via_default_route,
stale_time: kernel.stale_seconds.map_or(defaults.stale_time, |seconds| { stale_time: kernel.stale_seconds.map_or(defaults.stale_time, |seconds| {
Duration::from_secs(seconds.into()) Duration::from_secs(seconds.into())
@@ -452,12 +469,8 @@ fn parse_export_limits(
limits: &proto::ExportLimits, limits: &proto::ExportLimits,
) -> Result<Option<ExportLimits>, ImportProblem> { ) -> Result<Option<ExportLimits>, ImportProblem> {
let limits = ExportLimits { let limits = ExportLimits {
prefixes: if limits.prefixes.is_empty() { prefixes: parse_prefixes_if_any(&limits.prefixes)?,
None max_prefixes: given(limits.max_prefixes),
} else {
Some(parse_prefixes(&limits.prefixes)?)
},
max_prefixes: Some(limits.max_prefixes).filter(|limit| *limit > 0),
}; };
Ok((limits != ExportLimits::default()).then_some(limits)) Ok((limits != ExportLimits::default()).then_some(limits))
} }
@@ -472,38 +485,17 @@ fn parse_peer(peer: Peer, defaults: &PeerDefaults) -> Result<PeerConfig, PeerPro
return Err(PeerProblem::RemoteAsn); return Err(PeerProblem::RemoteAsn);
} }
let hold_time_secs = match peer.hold_time_seconds { let hold_time_secs = parse_hold_time(peer.hold_time_seconds)?;
None => DEFAULT_HOLD_TIME_SECS, let local_address = (!peer.local_address.is_empty())
// RFC 4271 §4.2: zero, or at least three seconds. .then(|| {
Some(1 | 2) => return Err(PeerProblem::HoldTime), IpAddr::from_str(&peer.local_address)
Some(secs) => u16::try_from(secs).map_err(|_| PeerProblem::HoldTime)?, .map_err(|_| PeerProblem::LocalAddress(peer.local_address.clone()))
}; })
.transpose()?;
let local_address = if peer.local_address.is_empty() { if local_address.is_some_and(|ip| ip.is_ipv4() != endpoint.ip.is_ipv4()) {
None
} else {
let ip = IpAddr::from_str(&peer.local_address)
.map_err(|_| PeerProblem::LocalAddress(peer.local_address.clone()))?;
if ip.is_ipv4() != endpoint.ip.is_ipv4() {
return Err(PeerProblem::AddressFamilyMismatch); return Err(PeerProblem::AddressFamilyMismatch);
} }
Some(ip) let families = parse_families(&peer.address_families)?;
};
let families = if peer.address_families.is_empty() {
FamilySet::supported()
} else {
peer.address_families
.iter()
.map(|family| match family.enum_value() {
Ok(proto::AddressFamily::IPV4_UNICAST) => Ok(AfiSafi::IPV4_UNICAST),
Ok(proto::AddressFamily::IPV6_UNICAST) => Ok(AfiSafi::IPV6_UNICAST),
Ok(proto::AddressFamily::ADDRESS_FAMILY_UNSPECIFIED) | Err(_) => {
Err(PeerProblem::AddressFamily)
}
})
.collect::<Result<_, _>>()?
};
let add_path = match peer.add_path.enum_value() { let add_path = match peer.add_path.enum_value() {
Ok(proto::AddPath::ADD_PATH_OFF) => None, Ok(proto::AddPath::ADD_PATH_OFF) => None,
@@ -513,20 +505,14 @@ fn parse_peer(peer: Peer, defaults: &PeerDefaults) -> Result<PeerConfig, PeerPro
Err(_) => return Err(PeerProblem::AddPath), Err(_) => return Err(PeerProblem::AddPath),
}; };
let import = match peer.import.as_ref() { let import = peer
Some(import) => parse_import(import)?, .import
None => ImportPolicy::default(), .as_ref()
}; .map(parse_import)
.transpose()?
.unwrap_or_default();
let role = match peer.role.enum_value() { let role = parse_role(peer.role)?;
Ok(proto::Role::ROLE_UNSPECIFIED) => None,
Ok(proto::Role::PROVIDER) => Some(PeerRole::Provider),
Ok(proto::Role::CUSTOMER) => Some(PeerRole::Customer),
Ok(proto::Role::PEER) => Some(PeerRole::Peer),
Ok(proto::Role::ROUTE_SERVER) => Some(PeerRole::RouteServer),
Ok(proto::Role::ROUTE_SERVER_CLIENT) => Some(PeerRole::RouteServerClient),
Err(_) => return Err(PeerProblem::Role),
};
if peer.require_role && role.is_none() { if peer.require_role && role.is_none() {
return Err(PeerProblem::RequireRoleWithoutRole); return Err(PeerProblem::RequireRoleWithoutRole);
} }
@@ -550,24 +536,74 @@ fn parse_peer(peer: Peer, defaults: &PeerDefaults) -> Result<PeerConfig, PeerPro
import, import,
role, role,
require_role: peer.require_role, require_role: peer.require_role,
export: match peer.export.as_ref() { export: peer
Some(own) => Arc::new(parse_export(own)?), .export
None => Arc::clone(&defaults.export), .as_ref()
}, .map(parse_export)
export_limits: match peer.export_limits.as_ref() { .transpose()?
Some(own) => parse_export_limits(own) .map_or_else(|| Arc::clone(&defaults.export), Arc::new),
// A block of the peer's own takes the place of the top-level one,
// an empty block included: that lifts the limits.
export_limits: peer
.export_limits
.as_ref()
.map(parse_export_limits)
.transpose()
.map_err(PeerProblem::ExportLimits)? .map_err(PeerProblem::ExportLimits)?
.map(Arc::new), .map_or_else(|| defaults.export_limits.clone(), |own| own.map(Arc::new)),
None => defaults.export_limits.clone(), graceful_restart: peer
}, .graceful_restart
graceful_restart: match peer.graceful_restart.as_ref() { .as_ref()
Some(own) => defaults.graceful_restart.overridden_by(own)?, .map(|own| defaults.graceful_restart.overridden_by(own))
None => defaults.graceful_restart, .transpose()?
}, .unwrap_or(defaults.graceful_restart),
groups: peer.groups, groups: peer.groups,
reflector_client: peer.route_reflector_client, reflector_client: peer.route_reflector_client,
passive: peer.passive, passive: peer.passive,
expected_routes: Some(peer.expected_routes).filter(|n| *n > 0), expected_routes: given(peer.expected_routes),
})
}
/// The hold time a peer is offered: the default where none is given.
fn parse_hold_time(seconds: Option<u32>) -> Result<u16, PeerProblem> {
match seconds {
None => Ok(DEFAULT_HOLD_TIME_SECS),
// RFC 4271 §4.2: zero, or at least three seconds.
Some(1 | 2) => Err(PeerProblem::HoldTime),
Some(secs) => u16::try_from(secs).map_err(|_| PeerProblem::HoldTime),
}
}
/// The address families offered to a peer: every one there is, unless
/// some are listed.
fn parse_families(
families: &[EnumOrUnknown<proto::AddressFamily>],
) -> Result<FamilySet, PeerProblem> {
if families.is_empty() {
return Ok(FamilySet::supported());
}
families
.iter()
.map(|family| match family.enum_value() {
Ok(proto::AddressFamily::IPV4_UNICAST) => Ok(AfiSafi::IPV4_UNICAST),
Ok(proto::AddressFamily::IPV6_UNICAST) => Ok(AfiSafi::IPV6_UNICAST),
Ok(proto::AddressFamily::ADDRESS_FAMILY_UNSPECIFIED) | Err(_) => {
Err(PeerProblem::AddressFamily)
}
})
.collect()
}
/// What the peer is to us, if that was said.
fn parse_role(role: EnumOrUnknown<proto::Role>) -> Result<Option<PeerRole>, PeerProblem> {
Ok(match role.enum_value() {
Ok(proto::Role::ROLE_UNSPECIFIED) => None,
Ok(proto::Role::PROVIDER) => Some(PeerRole::Provider),
Ok(proto::Role::CUSTOMER) => Some(PeerRole::Customer),
Ok(proto::Role::PEER) => Some(PeerRole::Peer),
Ok(proto::Role::ROUTE_SERVER) => Some(PeerRole::RouteServer),
Ok(proto::Role::ROUTE_SERVER_CLIENT) => Some(PeerRole::RouteServerClient),
Err(_) => return Err(PeerProblem::Role),
}) })
} }
@@ -584,7 +620,6 @@ fn parse_rpki(rpki: &proto::Rpki) -> Result<RpkiConfig, ConfigError> {
port => parse_endpoint(&cache.address, port), port => parse_endpoint(&cache.address, port),
}) })
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
let seconds = |seconds: u32| (seconds > 0).then(|| Duration::from_secs(seconds.into()));
Ok(RpkiConfig { Ok(RpkiConfig {
caches, caches,
refresh: seconds(rpki.refresh_seconds), refresh: seconds(rpki.refresh_seconds),
@@ -595,20 +630,20 @@ fn parse_rpki(rpki: &proto::Rpki) -> Result<RpkiConfig, ConfigError> {
fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProblem> { fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProblem> {
let defaults = ImportPolicy::default(); let defaults = ImportPolicy::default();
// A length of zero is the field left out. let longest = |field, value: u32, version: IpVersion, default: u8| {
let longest = |field, value: u32, version: IpVersion, default: u8| match value { given(value).map_or(Ok(default), |value| {
0 => Ok(default), u8::try_from(value)
value => u8::try_from(value)
.ok() .ok()
.filter(|length| *length <= version.bits()) .filter(|length| *length <= version.bits())
.ok_or(ImportProblem::PrefixLength { .ok_or(ImportProblem::PrefixLength {
field, field,
value, value,
longest: version.bits(), longest: version.bits(),
}), })
})
}; };
let prefixes = parse_prefixes(&import.prefixes)?; let prefixes = parse_prefixes_if_any(&import.prefixes)?;
let rules = import let rules = import
.rules .rules
.iter() .iter()
@@ -622,11 +657,8 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
Ok(ImportPolicy { Ok(ImportPolicy {
max_prefixes: Some(import.max_prefixes).filter(|limit| *limit > 0), max_prefixes: given(import.max_prefixes),
hold_down: match import.hold_down_seconds { hold_down: seconds(import.hold_down_seconds).unwrap_or(defaults.hold_down),
0 => defaults.hold_down,
seconds => Duration::from_secs(seconds.into()),
},
accept_bogons: import.accept_bogons, accept_bogons: import.accept_bogons,
accept_default_route: import.accept_default_route, accept_default_route: import.accept_default_route,
max_ipv4_length: longest( max_ipv4_length: longest(
@@ -646,7 +678,7 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
accept_aspa_invalid: import.accept_aspa_invalid, accept_aspa_invalid: import.accept_aspa_invalid,
accept_as_sets: import.accept_as_sets, accept_as_sets: import.accept_as_sets,
accept_third_party_next_hop: import.accept_third_party_next_hop, accept_third_party_next_hop: import.accept_third_party_next_hop,
accept_loopback_next_hop: false, accept_own_host_next_hop: false,
accept_blackholes: import.accept_blackholes, accept_blackholes: import.accept_blackholes,
own_prefixes: Arc::default(), own_prefixes: Arc::default(),
originated: Arc::default(), originated: Arc::default(),
@@ -660,13 +692,13 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
local_pref: import.local_pref, local_pref: import.local_pref,
rules, rules,
reflection: None, reflection: None,
max_as_path_length: match import.max_as_path_length { max_as_path_length: given(import.max_as_path_length)
0 => defaults.max_as_path_length, .map(|length| u16::try_from(length).map_err(|_| ImportProblem::PathLength(length)))
length => u16::try_from(length).map_err(|_| ImportProblem::PathLength(length))?, .transpose()?
}, .unwrap_or(defaults.max_as_path_length),
prefixes: Some(prefixes).filter(|prefixes| !prefixes.is_empty()), prefixes,
origin_asns: Some(import.origin_asns.iter().copied().collect()) origin_asns: (!import.origin_asns.is_empty())
.filter(|asns: &std::collections::HashSet<u32>| !asns.is_empty()), .then(|| import.origin_asns.iter().copied().collect()),
}) })
} }
@@ -681,17 +713,17 @@ fn parse_prefixes(ranges: &[proto::PrefixRange]) -> Result<PrefixSet, ImportProb
problem, problem,
})?; })?;
let longest = prefix.address_family().bits(); let longest = prefix.address_family().bits();
let max_length = match range.max_length { // Left out, the prefix itself is the longest allowed.
0 => prefix.length(), let max_length = given(range.max_length).map_or(Ok(prefix.length()), |max_length| {
max_length => u8::try_from(max_length) u8::try_from(max_length)
.ok() .ok()
.filter(|max| (prefix.length()..=longest).contains(max)) .filter(|max| (prefix.length()..=longest).contains(max))
.ok_or(ImportProblem::MaxLength { .ok_or(ImportProblem::MaxLength {
prefix: range.prefix.clone(), prefix: range.prefix.clone(),
max_length, max_length,
longest, longest,
})?, })
}; })?;
prefixes.allow(prefix, max_length); prefixes.allow(prefix, max_length);
} }
Ok(prefixes) Ok(prefixes)
@@ -700,21 +732,16 @@ fn parse_prefixes(ranges: &[proto::PrefixRange]) -> Result<PrefixSet, ImportProb
fn parse_rule(rule: &proto::ImportRule) -> Result<ImportRule, ImportProblem> { fn parse_rule(rule: &proto::ImportRule) -> Result<ImportRule, ImportProblem> {
Ok(ImportRule { Ok(ImportRule {
communities: Patterns::parse(&rule.match_communities)?, communities: Patterns::parse(&rule.match_communities)?,
prefixes: if rule.match_prefixes.is_empty() { prefixes: parse_prefixes_if_any(&rule.match_prefixes)?,
None
} else {
Some(parse_prefixes(&rule.match_prefixes)?)
},
local_pref: rule.local_pref, local_pref: rule.local_pref,
marks: Marks::parse(&[], &rule.add_communities, &rule.add_large_communities)?, marks: Marks::parse(&[], &rule.add_communities, &rule.add_large_communities)?,
}) })
} }
fn parse_port(port: u32) -> Result<u16, ConfigError> { fn parse_port(port: u32) -> Result<u16, ConfigError> {
match port { given(port).map_or(Ok(DEFAULT_BGP_PORT), |port| {
0 => Ok(DEFAULT_BGP_PORT), u16::try_from(port).map_err(|_| ConfigError::Port(port))
port => u16::try_from(port).map_err(|_| ConfigError::Port(port)), })
}
} }
fn parse_endpoint(address: &str, port: u32) -> Result<SocketAddr, ConfigError> { fn parse_endpoint(address: &str, port: u32) -> Result<SocketAddr, ConfigError> {
+15 -1
View File
@@ -15,7 +15,8 @@
use bgp_packet::community::{CommunityError, parse_large, parse_standard}; use bgp_packet::community::{CommunityError, parse_large, parse_standard};
use bgp_packet::message::{ use bgp_packet::message::{
CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity, PathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
LocalPrefPathAttribute, PathAttribute,
}; };
/// A standard community, or all those with one half or both left open: /// A standard community, or all those with one half or both left open:
@@ -186,6 +187,19 @@ impl Marks {
} }
} }
/// Marks `attributes` as a policy or one of its rules says: with `marks`
/// on their communities, and with `local_pref` as their LOCAL_PREF if one
/// is given, in place of whatever they had.
pub fn mark(attributes: &mut Vec<PathAttribute>, marks: &Marks, local_pref: Option<u32>) {
marks.apply(attributes);
if let Some(local_pref) = local_pref {
put(
attributes,
PathAttribute::LocalPref(LocalPrefPathAttribute(local_pref)),
);
}
}
/// Takes the standard communities out of `attributes`. /// Takes the standard communities out of `attributes`.
fn take_communities(attributes: &mut Vec<PathAttribute>) -> Vec<(u16, u16)> { fn take_communities(attributes: &mut Vec<PathAttribute>) -> Vec<(u16, u16)> {
let at = attributes let at = attributes
+338 -369
View File
@@ -2,391 +2,296 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use bgp_fib::sockdiag::TcpConnection;
use bgp_packet::message::Message; use bgp_packet::message::Message;
#[cfg(test)]
use prometheus::core::Collector;
use prometheus::{ use prometheus::{
GaugeVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, register_gauge_vec, GaugeVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, register_gauge_vec,
register_int_counter, register_int_counter_vec, register_int_gauge, register_int_gauge_vec, register_int_counter, register_int_counter_vec, register_int_gauge, register_int_gauge_vec,
}; };
pub static MESSAGES_SENT: LazyLock<IntCounterVec> = LazyLock::new(|| { /// What a declared metric puts on the registry: one collector for a
register_int_counter_vec!( /// metric, and one for each gauge of a group of them.
"bgp_messages_sent_total", #[cfg(test)]
"BGP messages sent, by type", trait Collectors {
&["type"] fn collectors(&self) -> Vec<Box<dyn Collector>>;
)
.expect("metric registration")
});
pub static MESSAGES_RECEIVED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_messages_received_total",
"BGP messages received, by type",
&["type"]
)
.expect("metric registration")
});
pub static UPDATE_ERRORS: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_update_errors_total",
"Errors in received UPDATE messages that did not reset the session, by how they were handled (RFC 7606)",
&["handling"]
)
.expect("metric registration")
});
pub static SESSIONS_ESTABLISHED_TOTAL: LazyLock<IntCounter> = LazyLock::new(|| {
register_int_counter!(
"bgp_sessions_established_total",
"Sessions that reached Established"
)
.expect("metric registration")
});
pub static SESSIONS_ESTABLISHED: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
"bgp_sessions_established",
"Sessions currently in Established"
)
.expect("metric registration")
});
pub static ROUTES_REJECTED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_routes_rejected_total",
"Routes announced by a peer and refused by its import policy, by why",
&["peer", "reason"]
)
.expect("metric registration")
});
pub static PEER_ROUTES: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
"bgp_peer_routes",
"Routes of an established peer that are in the routing table",
&["peer"]
)
.expect("metric registration")
});
pub static PEER_ADVERTISED: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
"bgp_peer_advertised_routes",
"Routes announced to an established peer and not withdrawn since",
&["peer"]
)
.expect("metric registration")
});
/// A session's TCP connection as the kernel reports it (`TCP_INFO`),
/// sampled when the metrics are scraped. Seconds, octets, segments, and
/// octets per second; the busy, limited and byte figures are cumulative
/// over the connection's life.
pub struct PeerTcp {
pub rtt_seconds: GaugeVec,
pub min_rtt_seconds: GaugeVec,
pub rtt_variance_seconds: GaugeVec,
pub cwnd_segments: IntGaugeVec,
pub mss_bytes: IntGaugeVec,
pub send_window_bytes: IntGaugeVec,
pub receive_window_bytes: IntGaugeVec,
pub send_queue_bytes: IntGaugeVec,
pub not_sent_bytes: IntGaugeVec,
pub receive_queue_bytes: IntGaugeVec,
pub unacked_segments: IntGaugeVec,
pub retransmitted_segments: IntGaugeVec,
pub lost_segments: IntGaugeVec,
pub busy_seconds: GaugeVec,
pub receive_window_limited_seconds: GaugeVec,
pub send_buffer_limited_seconds: GaugeVec,
pub sent_bytes: IntGaugeVec,
pub received_bytes: IntGaugeVec,
pub delivery_rate_bytes_per_second: IntGaugeVec,
pub pacing_rate_bytes_per_second: IntGaugeVec,
} }
impl PeerTcp { #[cfg(test)]
/// Forgets the peer's series, when its session has ended. impl<T: Collector + Clone + 'static> Collectors for T {
pub fn remove(&self, label: &str) { fn collectors(&self) -> Vec<Box<dyn Collector>> {
for gauge in [ vec![Box::new(self.clone())]
&self.rtt_seconds,
&self.min_rtt_seconds,
&self.rtt_variance_seconds,
&self.busy_seconds,
&self.receive_window_limited_seconds,
&self.send_buffer_limited_seconds,
] {
let _ = gauge.remove_label_values(&[label]);
}
for gauge in [
&self.cwnd_segments,
&self.mss_bytes,
&self.send_window_bytes,
&self.receive_window_bytes,
&self.send_queue_bytes,
&self.not_sent_bytes,
&self.receive_queue_bytes,
&self.unacked_segments,
&self.retransmitted_segments,
&self.lost_segments,
&self.sent_bytes,
&self.received_bytes,
&self.delivery_rate_bytes_per_second,
&self.pacing_rate_bytes_per_second,
] {
let _ = gauge.remove_label_values(&[label]);
}
} }
} }
pub static PEER_TCP: LazyLock<PeerTcp> = LazyLock::new(|| { /// Declares the metrics: a static for each, made on first use, and
let seconds = |name: &str, help: &str| { /// [`init`], which forces every one of them so that nothing is left out
register_gauge_vec!(format!("bgp_peer_tcp_{name}"), help, &["peer"]) /// of a scrape for not having been used yet. Each is declared once, here,
.expect("metric registration") /// so that the two cannot drift apart.
}; macro_rules! metrics {
let count = |name: &str, help: &str| { ($(
register_int_gauge_vec!(format!("bgp_peer_tcp_{name}"), help, &["peer"]) $(#[$meta:meta])*
.expect("metric registration") $name:ident: $type:ty = $register:expr;
}; )*) => {
PeerTcp { $(
rtt_seconds: seconds( $(#[$meta])*
"rtt_seconds", pub static $name: LazyLock<$type> =
"Smoothed round-trip time of the session's TCP connection", LazyLock::new(|| $register.expect("metric registration"));
), )*
min_rtt_seconds: seconds(
"min_rtt_seconds", /// Every metric declared above, as the collectors the registry
"Least round-trip time seen on the connection", /// holds, for the tests to check it against.
), #[cfg(test)]
rtt_variance_seconds: seconds( fn declared() -> Vec<Box<dyn Collector>> {
"rtt_variance_seconds", let mut all = Vec::new();
"Round-trip time variance of the connection", $(all.extend($name.collectors());)*
), all
cwnd_segments: count("cwnd_segments", "Congestion window, in segments"),
mss_bytes: count("mss_bytes", "Maximum segment size"),
send_window_bytes: count(
"send_window_bytes",
"The peer's receive window: what it lets us send",
),
receive_window_bytes: count(
"receive_window_bytes",
"Our receive window: what we let the peer send",
),
send_queue_bytes: count(
"send_queue_bytes",
"Octets written and not yet acknowledged by the peer",
),
not_sent_bytes: count("not_sent_bytes", "Octets written and not yet sent"),
receive_queue_bytes: count("receive_queue_bytes", "Octets received and not yet read"),
unacked_segments: count("unacked_segments", "Segments sent and not yet acknowledged"),
retransmitted_segments: count(
"retransmitted_segments",
"Segments retransmitted over the connection's life",
),
lost_segments: count("lost_segments", "Segments thought lost now"),
busy_seconds: seconds(
"busy_seconds",
"Time the connection had something to send, over its life",
),
receive_window_limited_seconds: seconds(
"receive_window_limited_seconds",
"Of the busy time, time held up by the peer's receive window",
),
send_buffer_limited_seconds: seconds(
"send_buffer_limited_seconds",
"Of the busy time, time held up by our send buffer being full",
),
sent_bytes: count("sent_bytes", "Octets sent over the connection's life"),
received_bytes: count(
"received_bytes",
"Octets received over the connection's life",
),
delivery_rate_bytes_per_second: count(
"delivery_rate_bytes_per_second",
"Delivery rate the kernel measured most recently",
),
pacing_rate_bytes_per_second: count(
"pacing_rate_bytes_per_second",
"Rate the kernel paces the connection at",
),
} }
});
/// The allocator's account of the daemon's memory, by `kind`: allocated, /// Forces registration of every metric so they appear in scrapes
/// active, metadata, resident, mapped, retained (jemalloc's `stats.*`). /// before their first observation: a counter that is not there
pub static MEMORY: LazyLock<IntGaugeVec> = LazyLock::new(|| { /// until it first counts hides that first count from `increase()`.
register_int_gauge_vec!( pub fn init() {
"bgp_memory_bytes", $(LazyLock::force(&$name);)*
"The daemon's memory as its allocator accounts for it, by kind",
&["kind"]
)
.expect("metric registration")
});
pub static PEER_MAX_PREFIXES: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
"bgp_peer_max_prefixes",
"The most routes a configured peer may have (import.max_prefixes), where there is a limit",
&["peer"]
)
.expect("metric registration")
});
pub static PREFIX_LIMIT_EXCEEDED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_prefix_limit_exceeded_total",
"Sessions ended because the peer went over its limit of routes",
&["peer"]
)
.expect("metric registration")
});
pub static EXPORT_LIMIT_EXCEEDED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_export_limit_exceeded_total",
"Sessions ended because more routes were to be announced to the peer than its export limit allows",
&["peer"]
)
.expect("metric registration")
});
pub static RPKI_VRPS: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
"bgp_rpki_vrps",
"Validated ROA payloads in use for route origin validation, by address family",
&["family"]
)
.expect("metric registration")
});
pub static RPKI_ASPAS: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
"bgp_rpki_aspas",
"Customer ASes with an ASPA in use for AS path verification"
)
.expect("metric registration")
});
pub static RPKI_UPDATES: LazyLock<IntCounter> = LazyLock::new(|| {
register_int_counter!("bgp_rpki_updates_total", "Times the VRPs or ASPAs changed")
.expect("metric registration")
});
pub static CONNECTIONS_REFUSED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_connections_refused_total",
"Incoming connections closed without a session, by why",
&["reason"]
)
.expect("metric registration")
});
pub static COLLISIONS: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_connection_collisions_total",
"Connection collisions resolved, by which connection was closed",
&["closed"]
)
.expect("metric registration")
});
pub static LOCAL_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!("bgp_local_routes", "Locally originated routes")
.expect("metric registration")
});
pub static RIB_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
"bgp_rib_routes",
"Routes learned from peers, counting each peer's separately"
)
.expect("metric registration")
});
pub static RIB_PREFIXES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
"bgp_rib_prefixes",
"Prefixes with at least one route, and so with a best route"
)
.expect("metric registration")
});
pub static RIB_ATTRIBUTE_SETS: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
"bgp_rib_attribute_sets",
"Distinct sets of path attributes held for the routes"
)
.expect("metric registration")
});
pub static RIB_JOURNAL_ENTRIES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
"bgp_rib_journal_entries",
"Changes of best route waiting for a follower to read them"
)
.expect("metric registration")
});
pub static ROUTES_EXPORTED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_routes_exported_total",
"Routes learned from one peer and announced to, or withdrawn from, another",
&["change"]
)
.expect("metric registration")
});
/// Whether each configured peer's session is up: what to alert on when a
/// peer goes away, which a count of sessions cannot say.
pub static PEER_UP: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
"bgp_peer_up",
"Whether the session with a configured peer is Established (1) or not (0)",
&["peer"]
)
.expect("metric registration")
});
pub static BUILD_INFO: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
"bgp_build_info",
"Always 1; the version of the daemon is in the label",
&["version"]
)
.expect("metric registration")
});
/// Forces registration of every metric so they appear in scrapes before
/// their first observation: a counter that is not there until it first
/// counts hides that first count from `increase()`.
pub fn init() {
LazyLock::force(&MESSAGES_SENT);
LazyLock::force(&MESSAGES_RECEIVED);
LazyLock::force(&UPDATE_ERRORS);
LazyLock::force(&SESSIONS_ESTABLISHED_TOTAL);
LazyLock::force(&SESSIONS_ESTABLISHED);
LazyLock::force(&ROUTES_REJECTED);
LazyLock::force(&PEER_ROUTES);
LazyLock::force(&PEER_MAX_PREFIXES);
LazyLock::force(&PREFIX_LIMIT_EXCEEDED);
LazyLock::force(&EXPORT_LIMIT_EXCEEDED);
LazyLock::force(&RPKI_VRPS);
LazyLock::force(&RPKI_ASPAS);
LazyLock::force(&RPKI_UPDATES);
LazyLock::force(&CONNECTIONS_REFUSED);
LazyLock::force(&COLLISIONS);
LazyLock::force(&LOCAL_ROUTES);
LazyLock::force(&RIB_ROUTES);
LazyLock::force(&RIB_PREFIXES);
LazyLock::force(&RIB_ATTRIBUTE_SETS);
LazyLock::force(&RIB_JOURNAL_ENTRIES);
LazyLock::force(&ROUTES_EXPORTED);
LazyLock::force(&PEER_UP);
LazyLock::force(&PEER_ADVERTISED);
LazyLock::force(&PEER_TCP);
LazyLock::force(&MEMORY);
BUILD_INFO BUILD_INFO
.with_label_values(&[env!("CARGO_PKG_VERSION")]) .with_label_values(&[env!("CARGO_PKG_VERSION")])
.set(1); .set(1);
}
};
}
metrics! {
MESSAGES_SENT: IntCounterVec = register_int_counter_vec!(
"bgp_messages_sent_total",
"BGP messages sent, by type",
&["type"]
);
MESSAGES_RECEIVED: IntCounterVec = register_int_counter_vec!(
"bgp_messages_received_total",
"BGP messages received, by type",
&["type"]
);
UPDATE_ERRORS: IntCounterVec = register_int_counter_vec!(
"bgp_update_errors_total",
"Errors in received UPDATE messages that did not reset the session, by how they were handled (RFC 7606)",
&["handling"]
);
SESSIONS_ESTABLISHED_TOTAL: IntCounter = register_int_counter!(
"bgp_sessions_established_total",
"Sessions that reached Established"
);
SESSIONS_ESTABLISHED: IntGauge = register_int_gauge!(
"bgp_sessions_established",
"Sessions currently in Established"
);
ROUTES_REJECTED: IntCounterVec = register_int_counter_vec!(
"bgp_routes_rejected_total",
"Routes announced by a peer and refused by its import policy, by why",
&["peer", "reason"]
);
PEER_ROUTES: IntGaugeVec = register_int_gauge_vec!(
"bgp_peer_routes",
"Routes of an established peer that are in the routing table",
&["peer"]
);
PEER_ADVERTISED: IntGaugeVec = register_int_gauge_vec!(
"bgp_peer_advertised_routes",
"Routes announced to an established peer and not withdrawn since",
&["peer"]
);
PEER_TCP: PeerTcp = PeerTcp::new();
/// The allocator's account of the daemon's memory, by `kind`: allocated,
/// active, metadata, resident, mapped, retained (jemalloc's `stats.*`).
MEMORY: IntGaugeVec = register_int_gauge_vec!(
"bgp_memory_bytes",
"The daemon's memory as its allocator accounts for it, by kind",
&["kind"]
);
PEER_MAX_PREFIXES: IntGaugeVec = register_int_gauge_vec!(
"bgp_peer_max_prefixes",
"The most routes a configured peer may have (import.max_prefixes), where there is a limit",
&["peer"]
);
PREFIX_LIMIT_EXCEEDED: IntCounterVec = register_int_counter_vec!(
"bgp_prefix_limit_exceeded_total",
"Sessions ended because the peer went over its limit of routes",
&["peer"]
);
EXPORT_LIMIT_EXCEEDED: IntCounterVec = register_int_counter_vec!(
"bgp_export_limit_exceeded_total",
"Sessions ended because more routes were to be announced to the peer than its export limit allows",
&["peer"]
);
RPKI_VRPS: IntGaugeVec = register_int_gauge_vec!(
"bgp_rpki_vrps",
"Validated ROA payloads in use for route origin validation, by address family",
&["family"]
);
RPKI_ASPAS: IntGauge = register_int_gauge!(
"bgp_rpki_aspas",
"Customer ASes with an ASPA in use for AS path verification"
);
RPKI_UPDATES: IntCounter = register_int_counter!(
"bgp_rpki_updates_total",
"Times the VRPs or ASPAs changed"
);
CONNECTIONS_REFUSED: IntCounterVec = register_int_counter_vec!(
"bgp_connections_refused_total",
"Incoming connections closed without a session, by why",
&["reason"]
);
COLLISIONS: IntCounterVec = register_int_counter_vec!(
"bgp_connection_collisions_total",
"Connection collisions resolved, by which connection was closed",
&["closed"]
);
LOCAL_ROUTES: IntGauge = register_int_gauge!(
"bgp_local_routes",
"Locally originated routes"
);
RIB_ROUTES: IntGauge = register_int_gauge!(
"bgp_rib_routes",
"Routes learned from peers, counting each peer's separately"
);
RIB_PREFIXES: IntGauge = register_int_gauge!(
"bgp_rib_prefixes",
"Prefixes with at least one route, and so with a best route"
);
RIB_ATTRIBUTE_SETS: IntGauge = register_int_gauge!(
"bgp_rib_attribute_sets",
"Distinct sets of path attributes held for the routes"
);
RIB_JOURNAL_ENTRIES: IntGauge = register_int_gauge!(
"bgp_rib_journal_entries",
"Changes of best route waiting for a follower to read them"
);
ROUTES_EXPORTED: IntCounterVec = register_int_counter_vec!(
"bgp_routes_exported_total",
"Routes learned from one peer and announced to, or withdrawn from, another",
&["change"]
);
/// Whether each configured peer's session is up: what to alert on when a
/// peer goes away, which a count of sessions cannot say.
PEER_UP: IntGaugeVec = register_int_gauge_vec!(
"bgp_peer_up",
"Whether the session with a configured peer is Established (1) or not (0)",
&["peer"]
);
BUILD_INFO: IntGaugeVec = register_int_gauge_vec!(
"bgp_build_info",
"Always 1; the version of the daemon is in the label",
&["version"]
);
}
/// The type of a gauge of [`PeerTcp`] by what it holds: a time, in
/// seconds, or a count.
macro_rules! tcp_gauge {
(seconds) => {
GaugeVec
};
(count) => {
IntGaugeVec
};
}
/// Registers a gauge of [`PeerTcp`], with the peer as its one label.
macro_rules! register_tcp_gauge {
(seconds, $name:expr, $help:expr) => {
register_gauge_vec!($name, $help, &["peer"])
};
(count, $name:expr, $help:expr) => {
register_int_gauge_vec!($name, $help, &["peer"])
};
}
/// What the kernel's figure becomes on the gauge: microseconds become
/// seconds, and a count is clamped into what a gauge holds.
macro_rules! tcp_gauge_value {
(seconds, $microseconds:expr) => {
$microseconds as f64 / 1e6
};
(count, $count:expr) => {
i64::try_from($count).unwrap_or(i64::MAX)
};
}
/// Declares the gauges of [`PeerTcp`], each named once with its help text
/// and the field of [`TcpConnection`] it is set from: the struct, how it is
/// made, and what sets and removes a peer's series all follow from the one
/// list. The gauge `name` is `bgp_peer_tcp_name`.
macro_rules! peer_tcp {
($connection:ident; $(
$name:ident: $kind:ident from $source:ident = $help:literal,
)*) => {
/// A session's TCP connection as the kernel reports it (`TCP_INFO`),
/// sampled when the metrics are scraped. Seconds, octets, segments, and
/// octets per second; the busy, limited and byte figures are cumulative
/// over the connection's life.
pub struct PeerTcp {
$(pub $name: tcp_gauge!($kind),)*
}
#[cfg(test)]
impl Collectors for PeerTcp {
fn collectors(&self) -> Vec<Box<dyn Collector>> {
vec![$(Box::new(self.$name.clone()),)*]
}
}
impl PeerTcp {
fn new() -> prometheus::Result<Self> {
Ok(Self {
$($name: register_tcp_gauge!(
$kind,
concat!("bgp_peer_tcp_", stringify!($name)),
$help
)?,)*
})
}
/// Sets the peer's gauges from what the kernel reports of its
/// connection.
pub fn set(&self, label: &str, $connection: &TcpConnection) {
let label = [label];
$(
self.$name
.with_label_values(&label)
.set(tcp_gauge_value!($kind, u64::from($connection.$source)));
)*
}
/// Forgets the peer's series, when its session has ended.
pub fn remove(&self, label: &str) {
$(let _ = self.$name.remove_label_values(&[label]);)*
}
}
};
}
peer_tcp! {
connection;
rtt_seconds: seconds from rtt = "Smoothed round-trip time of the session's TCP connection",
min_rtt_seconds: seconds from min_rtt = "Least round-trip time seen on the connection",
rtt_variance_seconds: seconds from rtt_variance = "Round-trip time variance of the connection",
cwnd_segments: count from cwnd = "Congestion window, in segments",
mss_bytes: count from mss = "Maximum segment size",
send_window_bytes: count from send_window = "The peer's receive window: what it lets us send",
receive_window_bytes: count from receive_window = "Our receive window: what we let the peer send",
send_queue_bytes: count from send_queue = "Octets written and not yet acknowledged by the peer",
not_sent_bytes: count from not_sent = "Octets written and not yet sent",
receive_queue_bytes: count from recv_queue = "Octets received and not yet read",
unacked_segments: count from unacked = "Segments sent and not yet acknowledged",
retransmitted_segments: count from total_retrans = "Segments retransmitted over the connection's life",
lost_segments: count from lost = "Segments thought lost now",
busy_seconds: seconds from busy = "Time the connection had something to send, over its life",
receive_window_limited_seconds: seconds from limited_by_receive_window = "Of the busy time, time held up by the peer's receive window",
send_buffer_limited_seconds: seconds from limited_by_send_buffer = "Of the busy time, time held up by our send buffer being full",
sent_bytes: count from bytes_sent = "Octets sent over the connection's life",
received_bytes: count from bytes_received = "Octets received over the connection's life",
delivery_rate_bytes_per_second: count from delivery_rate = "Delivery rate the kernel measured most recently",
pacing_rate_bytes_per_second: count from pacing_rate = "Rate the kernel paces the connection at",
} }
pub fn message_type_name(msg: &Message) -> &'static str { pub fn message_type_name(msg: &Message) -> &'static str {
@@ -398,3 +303,67 @@ pub fn message_type_name(msg: &Message) -> &'static str {
Message::RouteRefresh(_) => "route_refresh", Message::RouteRefresh(_) => "route_refresh",
} }
} }
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
/// Every metric family the declaration lists is on the registry once
/// `init` has run, each under a name of its own: the registry refuses
/// a second collector for any of them. The registry is the process's,
/// so what else is on it is not looked at.
#[test]
fn init_registers_every_declared_metric_family() {
init();
let declared = declared();
let names: HashSet<String> = declared
.iter()
.flat_map(|collector| collector.desc())
.map(|desc| desc.fq_name.clone())
.collect();
assert_eq!(names.len(), declared.len(), "a name is declared twice");
for collector in declared {
assert!(matches!(
prometheus::register(collector),
Err(prometheus::Error::AlreadyReg)
));
}
// A metric without labels is in a scrape from the start, which is
// what `init` is for; one with labels is there once it has one.
let scraped: HashSet<String> = prometheus::gather()
.iter()
.map(|family| family.name().to_string())
.collect();
for name in [
"bgp_sessions_established",
"bgp_rib_routes",
"bgp_build_info",
] {
assert!(scraped.contains(name), "{name} is not in a scrape");
}
}
/// The kernel's microseconds become seconds, and its counts are set as
/// they are; a peer whose session ended loses every series.
#[test]
fn a_connection_sets_and_a_removal_clears_every_gauge() {
let tcp = &*PEER_TCP;
let connection = TcpConnection {
rtt: 1500,
cwnd: 10,
bytes_sent: 1 << 40,
..TcpConnection::default()
};
tcp.set("metrics-test", &connection);
let label = ["metrics-test"];
assert_eq!(tcp.rtt_seconds.with_label_values(&label).get(), 0.0015);
assert_eq!(tcp.cwnd_segments.with_label_values(&label).get(), 10);
assert_eq!(tcp.sent_bytes.with_label_values(&label).get(), 1 << 40);
tcp.remove("metrics-test");
// There is nothing left to remove.
assert!(tcp.rtt_seconds.remove_label_values(&label).is_err());
assert!(tcp.sent_bytes.remove_label_values(&label).is_err());
}
}
+26 -32
View File
@@ -29,9 +29,7 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use std::time::Duration; use std::time::Duration;
use bgp_packet::message::{ use bgp_packet::message::{AS_TRANS, AsPathAttribute, NlriNextHop, PathAttribute};
AS_TRANS, AsPathAttribute, LocalPrefPathAttribute, NlriNextHop, PathAttribute,
};
use bgp_packet::open::BgpRole; use bgp_packet::open::BgpRole;
use bgp_primitives::constants::IpVersion; use bgp_primitives::constants::IpVersion;
use bgp_primitives::ip_prefix::IpPrefix; use bgp_primitives::ip_prefix::IpPrefix;
@@ -91,10 +89,10 @@ pub struct ImportPolicy {
/// who never asked for it. A route server's routes always have its /// who never asked for it. A route server's routes always have its
/// clients as next hops, and are taken either way. /// clients as next hops, and are taken either way.
pub accept_third_party_next_hop: bool, pub accept_third_party_next_hop: bool,
/// Take a next hop that is a loopback address, or one of our own: for /// Take a next hop that is on this host: a loopback address, or one of
/// routers that share a host, as those of a test do. Not a setting; /// our own. For routers that share a host, as those of a test do. Not
/// only [`ImportPolicy::permissive`] has it. /// a setting; only [`ImportPolicy::permissive`] has it.
pub accept_loopback_next_hop: bool, pub accept_own_host_next_hop: bool,
/// Take routes with the BLACKHOLE community for single addresses, and /// Take routes with the BLACKHOLE community for single addresses, and
/// have the kernel drop the traffic to them (RFC 7999). /// have the kernel drop the traffic to them (RFC 7999).
pub accept_blackholes: bool, pub accept_blackholes: bool,
@@ -174,13 +172,7 @@ impl ImportRule {
} }
pub fn apply(&self, attributes: &mut Vec<PathAttribute>) { pub fn apply(&self, attributes: &mut Vec<PathAttribute>) {
self.marks.apply(attributes); marks::mark(attributes, &self.marks, self.local_pref);
if let Some(local_pref) = self.local_pref {
marks::put(
attributes,
PathAttribute::LocalPref(LocalPrefPathAttribute(local_pref)),
);
}
} }
} }
@@ -203,7 +195,7 @@ impl Default for ImportPolicy {
accept_aspa_invalid: false, accept_aspa_invalid: false,
accept_as_sets: false, accept_as_sets: false,
accept_third_party_next_hop: false, accept_third_party_next_hop: false,
accept_loopback_next_hop: false, accept_own_host_next_hop: false,
accept_blackholes: false, accept_blackholes: false,
own_prefixes: Arc::default(), own_prefixes: Arc::default(),
originated: Arc::default(), originated: Arc::default(),
@@ -233,7 +225,7 @@ impl ImportPolicy {
accept_aspa_invalid: true, accept_aspa_invalid: true,
accept_as_sets: true, accept_as_sets: true,
accept_third_party_next_hop: true, accept_third_party_next_hop: true,
accept_loopback_next_hop: true, accept_own_host_next_hop: true,
accept_blackholes: false, accept_blackholes: false,
..Self::default() ..Self::default()
} }
@@ -256,13 +248,7 @@ impl ImportPolicy {
if let Some(scrub) = self.scrub { if let Some(scrub) = self.scrub {
scrub.apply(attributes); scrub.apply(attributes);
} }
self.marks.apply(attributes); marks::mark(attributes, &self.marks, self.local_pref);
if let Some(local_pref) = self.local_pref {
marks::put(
attributes,
PathAttribute::LocalPref(LocalPrefPathAttribute(local_pref)),
);
}
} }
} }
@@ -660,11 +646,24 @@ impl ImportPolicy {
return Err(Rejection::BogonPrefix); return Err(Rejection::BogonPrefix);
} }
} }
self.listed_or_ours(prefix, PrefixSet::allows)
}
/// Whether `prefix` is one the peer may announce by its list, where it
/// has one, and else not one of ours. `inside` says what being in a
/// set means: [`PrefixSet::allows`] for a route, which is bound by the
/// lengths allowed, [`PrefixSet::covers`] for a blackhole request,
/// which is for one address whatever the lengths.
fn listed_or_ours(
&self,
prefix: &IpPrefix,
inside: fn(&PrefixSet, &IpPrefix) -> bool,
) -> Result<(), Rejection> {
match &self.prefixes { match &self.prefixes {
Some(allowed) if !allowed.allows(prefix) => Err(Rejection::PrefixNotAllowed), Some(allowed) if !inside(allowed, prefix) => Err(Rejection::PrefixNotAllowed),
// Said to be this peer's to announce, ours or not. // Said to be this peer's to announce, ours or not.
Some(_) => Ok(()), Some(_) => Ok(()),
None if self.own_prefixes.allows(prefix) => Err(Rejection::OwnPrefix), None if inside(&self.own_prefixes, prefix) => Err(Rejection::OwnPrefix),
None => Ok(()), None => Ok(()),
} }
} }
@@ -690,12 +689,7 @@ impl ImportPolicy {
if !self.accept_bogons && is_bogon_prefix(prefix) { if !self.accept_bogons && is_bogon_prefix(prefix) {
return Err(Rejection::BogonPrefix); return Err(Rejection::BogonPrefix);
} }
match &self.prefixes { self.listed_or_ours(prefix, PrefixSet::covers)
Some(allowed) if !allowed.covers(prefix) => Err(Rejection::PrefixNotAllowed),
Some(_) => Ok(()),
None if self.own_prefixes.covers(prefix) => Err(Rejection::OwnPrefix),
None => Ok(()),
}
} }
/// Judges the next hop of routes of IP version `routes`. /// Judges the next hop of routes of IP version `routes`.
@@ -735,7 +729,7 @@ impl ImportPolicy {
let usable = !address.is_unspecified() let usable = !address.is_unspecified()
&& !address.is_multicast() && !address.is_multicast()
&& address != IpAddr::V4(Ipv4Addr::BROADCAST) && address != IpAddr::V4(Ipv4Addr::BROADCAST)
&& (self.accept_loopback_next_hop && (self.accept_own_host_next_hop
|| (!address.is_loopback() && address != from.local_address)); || (!address.is_loopback() && address != from.local_address));
if !usable { if !usable {
return Err(Rejection::NextHopUnusable); return Err(Rejection::NextHopUnusable);
+13 -39
View File
@@ -29,9 +29,9 @@ use bgp_primitives::constants::IpVersion;
use bgp_primitives::ip_prefix::{IpPrefix, Nlri}; use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use bgp_rib::table::{PeerIndex, RibTable}; use bgp_rib::table::{PeerIndex, RibTable};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{info, warn};
use crate::address::PeerAddress; use crate::address::PeerAddress;
use crate::rpki::{by_rib_peer, group_by_peer, revalidate_all};
use crate::session::SessionRegistry; use crate::session::SessionRegistry;
/// How long after the interfaces change the table is looked through: /// How long after the interfaces change the table is looked through:
@@ -130,37 +130,18 @@ pub(crate) fn spawn_revalidator(
continue; continue;
}; };
let sessions = registry.list(); let sessions = registry.list();
// By peer, for looking up as often as there are routes. let neighbors = by_rib_peer(
let mut neighbors: Vec<Option<Neighbor>> = Vec::new(); sessions
for session in &sessions { .iter()
let at = session.rib_peer.as_usize(); .map(|session| (session.rib_peer, neighbor_at(session.peer.addr))),
if neighbors.len() <= at {
neighbors.resize(at + 1, None);
}
neighbors[at] = Some(neighbor_at(session.peer.addr));
}
let table = Arc::clone(&rib);
// Looks at every route: not for a thread that sessions run on.
let found =
tokio::task::spawn_blocking(move || now_unreachable(&table, &topology, &neighbors))
.await;
let mut suspects = match found {
Ok(suspects) => suspects,
Err(e) => {
warn!(err = %e, "Looking for routes whose next hop is gone failed");
HashMap::new()
}
};
info!(
now_unreachable = suspects.values().map(Vec::len).sum::<usize>(),
sessions = sessions.len(),
"The machine's interfaces or routes changed; looking at next hops again"
); );
// Every session, also those with nothing found: each has its revalidate_all(
// held routes to look at again. sessions,
for session in sessions { &rib,
session.revalidate(suspects.remove(&session.rib_peer).unwrap_or_default()); "The machine's interfaces or routes changed; looking at next hops again",
} move |rib| now_unreachable(rib, &topology, &neighbors),
)
.await;
} }
}) })
} }
@@ -211,14 +192,7 @@ fn now_unreachable(
.is_ok() .is_ok()
}) })
}); });
let mut by_peer: HashMap<PeerIndex, Vec<Nlri>> = HashMap::new(); group_by_peer(routes)
for route in routes {
by_peer.entry(route.peer).or_default().push(Nlri {
prefix: route.prefix,
path_id: route.path_id,
});
}
by_peer
} }
#[cfg(test)] #[cfg(test)]
+192 -55
View File
@@ -22,7 +22,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use bgp_primitives::ip_prefix::{IpPrefix, Nlri}; use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use bgp_rib::table::{AttrSet, PeerIndex, RibTable}; use bgp_rib::table::{AttrSet, PeerIndex, RibTable, Route};
use bgp_rpki::{Algorithm, AspaTable, Changed, PathValidity, Rpki, Update, Validity, VrpTable}; use bgp_rpki::{Algorithm, AspaTable, Changed, PathValidity, Rpki, Update, Validity, VrpTable};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{info, warn}; use tracing::{info, warn};
@@ -239,50 +239,90 @@ pub(crate) fn spawn_revalidator(
RPKI_UPDATES.inc(); RPKI_UPDATES.inc();
tokio::time::sleep(SETTLE).await; tokio::time::sleep(SETTLE).await;
let sessions: Vec<(SessionHandle, Subject)> = registry // The sessions whose routes are subject to the checks at all.
let sessions: Vec<SessionHandle> = registry
.list() .list()
.into_iter() .into_iter()
.filter_map(|session| { .filter(|session| session.validated().is_some())
let subject = session.validated()?;
Some((session, subject))
})
.collect(); .collect();
// By peer, for looking up as often as there are routes. let subjects = by_rib_peer(
let mut subjects: Vec<Option<Subject>> = Vec::new(); sessions
for (session, subject) in &sessions { .iter()
let at = session.rib_peer.as_usize(); .filter_map(|session| Some((session.rib_peer, session.validated()?))),
if subjects.len() <= at { );
subjects.resize(at + 1, None); let what = format!(
"RPKI data changed, to {} VRPs and {} ASPAs; revalidating routes",
update.vrps.len(),
update.aspas.len()
);
revalidate_all(sessions, &rib, &what, move |rib| {
newly_invalid(rib, &update, &subjects)
})
.await;
} }
subjects[at] = Some(*subject); })
}
/// `what` of each session, by its peer's number in the table, for looking
/// up as often as there are routes; `None` where no session has that
/// number.
pub(crate) fn by_rib_peer<T>(sessions: impl IntoIterator<Item = (PeerIndex, T)>) -> Vec<Option<T>> {
let mut by_peer: Vec<Option<T>> = Vec::new();
for (peer, what) in sessions {
let at = peer.as_usize();
if by_peer.len() <= at {
by_peer.resize_with(at + 1, || None);
} }
let (vrps, aspas) = (update.vrps.len(), update.aspas.len()); by_peer[at] = Some(what);
let rib = Arc::clone(&rib); }
// Looks at every route, or at every prefix at least: not for a by_peer
// thread that sessions run on. }
let found =
tokio::task::spawn_blocking(move || newly_invalid(&rib, &update, &subjects)).await; /// `routes` as what each session is to take out of the table, by peer.
pub(crate) fn group_by_peer(
routes: impl IntoIterator<Item = Route>,
) -> HashMap<PeerIndex, Vec<Nlri>> {
let mut by_peer: HashMap<PeerIndex, Vec<Nlri>> = HashMap::new();
for route in routes {
by_peer.entry(route.peer).or_default().push(Nlri {
prefix: route.prefix,
path_id: route.path_id,
});
}
by_peer
}
/// One round of revalidation, for whichever data changed: `find` looks
/// through the table for the routes that are to go, by peer, and every one
/// of `sessions` is told of its own, also those with nothing found, since
/// each has its held routes to look at again. `what` is for the log.
pub(crate) async fn revalidate_all<F>(
sessions: Vec<SessionHandle>,
rib: &Arc<RibTable>,
what: &str,
find: F,
) where
F: FnOnce(&RibTable) -> HashMap<PeerIndex, Vec<Nlri>> + Send + 'static,
{
let table = Arc::clone(rib);
// Looks at every route, or at every prefix at least: not for a thread
// that sessions run on.
let found = tokio::task::spawn_blocking(move || find(&table)).await;
let mut suspects = match found { let mut suspects = match found {
Ok(suspects) => suspects, Ok(suspects) => suspects,
Err(e) => { Err(e) => {
warn!(err = %e, "Looking for routes that are now invalid failed"); warn!(err = %e, "Looking through the table for routes to take out failed");
HashMap::new() HashMap::new()
} }
}; };
info!( info!(
vrps, found = suspects.values().map(Vec::len).sum::<usize>(),
aspas,
now_invalid = suspects.values().map(Vec::len).sum::<usize>(),
sessions = sessions.len(), sessions = sessions.len(),
"RPKI data changed; revalidating routes" "{what}"
); );
// Every session, also those with nothing found: each has its for session in sessions {
// held routes to look at again.
for (session, _) in sessions {
session.revalidate(suspects.remove(&session.rib_peer).unwrap_or_default()); session.revalidate(suspects.remove(&session.rib_peer).unwrap_or_default());
} }
}
})
} }
/// The routes in the table that `data` finds invalid, by peer. `subjects` /// The routes in the table that `data` finds invalid, by peer. `subjects`
@@ -306,42 +346,54 @@ fn newly_invalid(
path.iter().any(|asn| changed.aspa_customers.contains(asn)) path.iter().any(|asn| changed.aspa_customers.contains(asn))
}; };
let path = RefCell::new(Vec::new()); let path = RefCell::new(Vec::new());
// What each peer's routes are checked against: the data, for the
// checks its routes are subject to.
let checks: Vec<Option<Checks<'_>>> = subjects
.iter()
.map(|subject| {
subject.map(|subject| Checks {
vrps: subject.origin.then_some(&*data.vrps),
aspas: subject.path.map(|check| (&*data.aspas, check)),
next_hops: None,
})
})
.collect();
let routes = rib.routes_where(usize::MAX, |route| { let routes = rib.routes_where(usize::MAX, |route| {
let Some(Some(subject)) = subjects.get(route.peer.as_usize()) else { let Some(Some(checks)) = checks.get(route.peer.as_usize()) else {
return false; return false;
}; };
if subject.origin
&& vrps_may_matter
&& changed.is_none_or(|changed| changed.vrps.covers(&route.prefix))
{
let origin = route.attrs.key().origin();
if data.vrps.validate(&route.prefix, origin) == Validity::Invalid {
return true;
}
}
if let Some(check) = subject.path
&& aspas_may_matter
{
let path = &mut *path.borrow_mut(); let path = &mut *path.borrow_mut();
let has_set = route.attrs.as_path(path); // Only the checks whose verdict the change can have altered for
return changed.is_none_or(|changed| on_changed_path(changed, path)) // this route are made.
&& check.verify(&data.aspas, path, has_set) == PathValidity::Invalid; let checks = Checks {
} vrps: checks.vrps.filter(|_| {
false vrps_may_matter && changed.is_none_or(|changed| changed.vrps.covers(&route.prefix))
}),
aspas: checks.aspas.filter(|_| {
aspas_may_matter
&& changed.is_none_or(|changed| {
route.attrs.as_path(path);
on_changed_path(changed, path)
})
}),
next_hops: None,
};
checks.rejection(&route.prefix, route.attrs, path).is_some()
}); });
let mut by_peer: HashMap<PeerIndex, Vec<Nlri>> = HashMap::new(); group_by_peer(routes)
for route in routes {
by_peer.entry(route.peer).or_default().push(Nlri {
prefix: route.prefix,
path_id: route.path_id,
});
}
by_peer
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::net::Ipv4Addr;
use bgp_packet::message::{
AsPathAttribute, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
};
use bgp_rib::table::PeerInfo;
use bgp_rpki::Vrp;
use super::*; use super::*;
fn aspas() -> AspaTable { fn aspas() -> AspaTable {
@@ -463,4 +515,89 @@ mod tests {
}) })
); );
} }
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
/// Where it is known which VRPs came or went, only the routes those
/// cover are looked at: the others are as valid or invalid as they
/// were, and were dealt with then. Without that knowledge every route
/// is looked at; and a peer whose routes are not subject to the check
/// has none found.
#[test]
fn only_routes_a_change_can_have_made_invalid_are_found() {
let table = RibTable::new(2);
let peer = table
.add_peer(PeerInfo {
asn: 65001,
router_id: Ipv4Addr::new(10, 0, 0, 1),
address: Ipv4Addr::new(192, 0, 2, 1).into(),
interface: 0,
ibgp: false,
reflector_client: false,
})
.unwrap();
let announce = |route: &str, origin: u32| {
let attrs = table
.attrs()
.intern(
&[
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![65001, origin]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
],
None,
)
.unwrap();
table.announce(peer, &attrs, &[prefix(route)]);
};
announce("10.0.0.0/24", 65010);
announce("10.1.0.0/24", 65011);
// Both prefixes are someone else's to originate.
let vrp = |covered: &str| Vrp {
prefix: prefix(covered),
max_length: 24,
asn: 65009,
};
let update = Update {
version: 2,
vrps: Arc::new(VrpTable::new([vrp("10.0.0.0/24"), vrp("10.1.0.0/24")])),
aspas: Arc::new(AspaTable::default()),
changed: Some(Changed {
vrps: Arc::new(VrpTable::new([vrp("10.1.0.0/24")])),
aspa_customers: Arc::default(),
}),
};
let origins = by_rib_peer([(
peer,
Subject {
origin: true,
path: None,
},
)]);
let found = newly_invalid(&table, &update, &origins);
assert_eq!(
found.get(&peer).map(Vec::as_slice),
Some(&[Nlri::from(prefix("10.1.0.0/24"))][..])
);
let whole = Update {
changed: None,
..update
};
let found = newly_invalid(&table, &whole, &origins);
let routes = found.get(&peer).expect("the peer's routes");
assert_eq!(routes.len(), 2);
assert!(routes.contains(&Nlri::from(prefix("10.0.0.0/24"))));
let paths_only = by_rib_peer([(
peer,
Subject {
origin: false,
path: None,
},
)]);
assert!(newly_invalid(&table, &whole, &paths_only).is_empty());
}
} }
+240 -182
View File
@@ -9,6 +9,7 @@ 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};
use bgp_fib::sockdiag::TcpConnection;
use bgp_packet::community; use bgp_packet::community;
use bgp_packet::message::{ use bgp_packet::message::{
AsPathAttribute, ExtendedCommunity, NlriNextHop, OriginPathAttribute, PathAttribute, AsPathAttribute, ExtendedCommunity, NlriNextHop, OriginPathAttribute, PathAttribute,
@@ -18,7 +19,8 @@ use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::table::{AttrSet, PeerIndex, RibTable, Route, RouteRef, SelectionKey}; use bgp_rib::table::{AttrSet, PeerIndex, RibTable, Route, RouteRef, SelectionKey};
use bgp_rib::{LocalRoute, RibStopped}; use bgp_rib::{LocalRoute, RibStopped};
use bgp_rpki::{CacheState, Update}; use bgp_rpki::{CacheState, Update};
use bgp_server::address::{PeerAddress, socket_text}; use bgp_server::address::{names_peer, socket_text};
use bgp_server::family::FamilySet;
use bgp_server::policy::PeerRole; use bgp_server::policy::PeerRole;
use bgp_server::restart::{Agreed, Restarts}; use bgp_server::restart::{Agreed, Restarts};
use bgp_server::rpki::PathCheck; use bgp_server::rpki::PathCheck;
@@ -148,51 +150,46 @@ impl From<&Agreed> for GracefulRestartView {
impl From<&SessionHandle> for PeerView { impl From<&SessionHandle> for PeerView {
fn from(s: &SessionHandle) -> Self { fn from(s: &SessionHandle) -> Self {
let families =
|families: &FamilySet| Some(families.iter().map(|f| f.to_string()).collect());
Self { Self {
name: s.name.clone(),
peer_addr: socket_text(s.peer.addr),
peer_asn: s.peer.asn,
state: "Established".into(),
direction: Some(s.direction.as_str().into()), direction: Some(s.direction.as_str().into()),
local_addr: Some(socket_text(s.local_addr)), local_addr: Some(socket_text(s.local_addr)),
established_at_unix: Some(unix_secs(s.established_at)), established_at_unix: Some(unix_secs(s.established_at)),
uptime_secs: Some(s.established_at.elapsed().map(|d| d.as_secs()).unwrap_or(0)), uptime_secs: Some(s.established_at.elapsed().map(|d| d.as_secs()).unwrap_or(0)),
four_octet_asn: Some(s.four_octet_asn), four_octet_asn: Some(s.four_octet_asn),
extended_next_hop: Some(s.extended_next_hop), extended_next_hop: Some(s.extended_next_hop),
address_families: Some(s.families.iter().map(|f| f.to_string()).collect()), address_families: families(&s.families),
add_path_receive: Some(s.add_path_receive.iter().map(|f| f.to_string()).collect()), add_path_receive: families(&s.add_path_receive),
add_path_send: Some(s.add_path_send.iter().map(|f| f.to_string()).collect()), add_path_send: families(&s.add_path_send),
routes: Some(s.counters.routes()), routes: Some(s.counters.routes()),
rejected_routes: Some(s.counters.rejected()), rejected_routes: Some(s.counters.rejected()),
advertised_routes: Some(s.counters.advertised()), advertised_routes: Some(s.counters.advertised()),
held_routes: Some(s.counters.held()), held_routes: Some(s.counters.held()),
expected_routes: None,
role: s.role.map(PeerRole::as_str), role: s.role.map(PeerRole::as_str),
role_confirmed: s.role.map(|_| s.role_confirmed), role_confirmed: s.role.map(|_| s.role_confirmed),
last_error: None,
retry_at_unix: None,
graceful_restart: s.graceful_restart.as_ref().map(GracefulRestartView::from), graceful_restart: s.graceful_restart.as_ref().map(GracefulRestartView::from),
stale_routes: Some(s.counters.stale()).filter(|stale| *stale > 0), stale_routes: Some(s.counters.stale()).filter(|stale| *stale > 0),
stale_until_unix: None, ..Self::known(
s.name.clone(),
socket_text(s.peer.addr),
s.peer.asn,
"Established",
)
} }
} }
} }
impl PeerView { impl PeerView {
fn from_configured(peer: &ConfiguredPeer, state: &DialState) -> Self { /// What is known of any peer, with or without a session: who it is
let (state_name, last_error, retry_at_unix) = match state { /// and where it stands. Everything else is `None`, for the caller to
DialState::Idle => ("Idle", None, None), /// fill in what it knows besides.
DialState::Connecting => ("Connecting", None, None), fn known(name: Option<String>, peer_addr: String, peer_asn: u32, state: &str) -> Self {
DialState::Backoff { until, last_error } => {
("Backoff", Some(last_error.clone()), Some(unix_secs(*until)))
}
DialState::Passive { last_error } => ("Passive", last_error.clone(), None),
};
Self { Self {
name: Some(peer.name.clone()), name,
peer_addr: peer.endpoint.with_port(), peer_addr,
peer_asn: peer.remote_asn, peer_asn,
state: state_name.into(), state: state.into(),
direction: None, direction: None,
local_addr: None, local_addr: None,
established_at_unix: None, established_at_unix: None,
@@ -206,16 +203,39 @@ impl PeerView {
rejected_routes: None, rejected_routes: None,
advertised_routes: None, advertised_routes: None,
held_routes: None, held_routes: None,
expected_routes: peer.expected_routes, expected_routes: None,
role: peer.role.map(PeerRole::as_str), role: None,
role_confirmed: None, role_confirmed: None,
last_error, last_error: None,
retry_at_unix, retry_at_unix: None,
graceful_restart: None, graceful_restart: None,
stale_routes: None, stale_routes: None,
stale_until_unix: None, stale_until_unix: None,
} }
} }
fn from_configured(peer: &ConfiguredPeer, state: &DialState) -> Self {
let (state_name, last_error, retry_at_unix) = match state {
DialState::Idle => ("Idle", None, None),
DialState::Connecting => ("Connecting", None, None),
DialState::Backoff { until, last_error } => {
("Backoff", Some(last_error.clone()), Some(unix_secs(*until)))
}
DialState::Passive { last_error } => ("Passive", last_error.clone(), None),
};
Self {
expected_routes: peer.expected_routes,
role: peer.role.map(PeerRole::as_str),
last_error,
retry_at_unix,
..Self::known(
Some(peer.name.clone()),
peer.endpoint.with_port(),
peer.remote_asn,
state_name,
)
}
}
} }
/// Configured peers first (with their live session when established), then /// Configured peers first (with their live session when established), then
@@ -541,9 +561,7 @@ impl PeerNames {
fn matching(&self, wanted: &str) -> Vec<PeerIndex> { fn matching(&self, wanted: &str) -> Vec<PeerIndex> {
self.peers self.peers
.iter() .iter()
.filter(|(_, (peer, name, _))| { .filter(|(_, (peer, name, _))| names_peer(wanted, name.as_deref(), peer.addr.into()))
name.as_deref() == Some(wanted) || PeerAddress::from(peer.addr).is_called(wanted)
})
.map(|(index, _)| *index) .map(|(index, _)| *index)
.collect() .collect()
} }
@@ -1226,7 +1244,7 @@ pub async fn rejected_routes(
query query
.peer .peer
.as_deref() .as_deref()
.is_none_or(|wanted| record.label() == wanted || address.is_called(wanted)) .is_none_or(|wanted| names_peer(wanted, Some(record.label()), *address))
}) })
.map(|(address, record)| RejectionsView { .map(|(address, record)| RejectionsView {
peer: record.label().to_string(), peer: record.label().to_string(),
@@ -1256,6 +1274,168 @@ pub async fn rejected_routes(
Json(peers) Json(peers)
} }
/// `GET /api/tcp?peer=NAME`: what the kernel knows of the TCP connection of
/// the session with that peer.
#[derive(Debug, Deserialize)]
pub struct TcpQuery {
/// The peer whose session it is: its configured name, or its address.
/// The address with its port, as `peer_addr` of `/api/peers` has it,
/// names it too.
pub peer: String,
}
/// A session's TCP connection, as the kernel reports it (Linux only).
/// Times in milliseconds, sizes in octets, rates in octets per second.
#[derive(Debug, Serialize)]
pub struct TcpView {
pub state: &'static str,
pub congestion: Option<String>,
pub rtt_ms: f64,
pub rtt_variance_ms: f64,
pub min_rtt_ms: f64,
pub rto_ms: f64,
pub mss: u32,
pub pmtu: u32,
pub cwnd_segments: u32,
pub ssthresh_segments: Option<u32>,
/// The peer's receive window: what it lets us send.
pub send_window: u32,
/// Our receive window: what we let the peer send.
pub receive_window: u32,
pub receive_space: u32,
pub send_window_scale: u8,
pub receive_window_scale: u8,
/// Octets written by us and not yet acknowledged, and of those the
/// octets not even sent yet.
pub send_queue: u32,
pub not_sent: u32,
pub recv_queue: u32,
pub unacked_segments: u32,
pub retrans_segments: u32,
pub lost_segments: u32,
pub total_retrans: u32,
pub bytes_retrans: u64,
pub pacing_rate: u64,
pub delivery_rate: u64,
pub bytes_sent: u64,
pub bytes_acked: u64,
pub bytes_received: u64,
pub segments_out: u32,
pub segments_in: u32,
/// How long the connection had something to send, and of that how
/// long it was held up by the peer's window and by our send buffer.
pub busy_ms: f64,
pub limited_by_receive_window_ms: f64,
pub limited_by_send_buffer_ms: f64,
/// The same two, as shares of the busy time, 0 to 1.
pub limited_by_receive_window_share: f64,
pub limited_by_send_buffer_share: f64,
pub last_data_sent_ms: u32,
pub last_data_received_ms: u32,
pub last_ack_received_ms: u32,
}
impl From<&TcpConnection> for TcpView {
fn from(c: &TcpConnection) -> Self {
// The kernel's times are in microseconds.
let ms = |us: u64| us as f64 / 1000.0;
// A connection that was never busy was never held up: a share of
// nothing is nothing, not a division by zero.
let share = |part: u64| {
if c.busy == 0 {
0.0
} else {
part as f64 / c.busy as f64
}
};
Self {
state: c.state_name(),
congestion: c.congestion.clone(),
rtt_ms: ms(c.rtt.into()),
rtt_variance_ms: ms(c.rtt_variance.into()),
min_rtt_ms: ms(c.min_rtt.into()),
rto_ms: ms(c.rto.into()),
mss: c.mss,
pmtu: c.pmtu,
cwnd_segments: c.cwnd,
ssthresh_segments: c.ssthresh,
send_window: c.send_window,
receive_window: c.receive_window,
receive_space: c.receive_space,
send_window_scale: c.send_window_scale,
receive_window_scale: c.receive_window_scale,
send_queue: c.send_queue,
not_sent: c.not_sent,
recv_queue: c.recv_queue,
unacked_segments: c.unacked,
retrans_segments: c.retrans,
lost_segments: c.lost,
total_retrans: c.total_retrans,
bytes_retrans: c.bytes_retrans,
pacing_rate: c.pacing_rate,
delivery_rate: c.delivery_rate,
bytes_sent: c.bytes_sent,
bytes_acked: c.bytes_acked,
bytes_received: c.bytes_received,
segments_out: c.segments_out,
segments_in: c.segments_in,
busy_ms: ms(c.busy),
limited_by_receive_window_ms: ms(c.limited_by_receive_window),
limited_by_send_buffer_ms: ms(c.limited_by_send_buffer),
limited_by_receive_window_share: share(c.limited_by_receive_window),
limited_by_send_buffer_share: share(c.limited_by_send_buffer),
last_data_sent_ms: c.last_data_sent_ms,
last_data_received_ms: c.last_data_received_ms,
last_ack_received_ms: c.last_ack_received_ms,
}
}
}
/// The answer to `/api/tcp`.
#[derive(Debug, Serialize)]
pub struct TcpAnswer {
/// Whether the kernel could be asked and knew the connection.
pub available: bool,
/// Why not, if not.
pub reason: Option<String>,
pub connection: Option<TcpView>,
}
pub async fn tcp(
State(state): State<AppState>,
Query(query): Query<TcpQuery>,
) -> Result<Json<TcpAnswer>, QueryError> {
let session = state
.registry
.list()
.into_iter()
.find(|s| {
names_peer(&query.peer, s.name.as_deref(), s.peer.addr.into())
|| socket_text(s.peer.addr) == query.peer
})
.ok_or_else(|| QueryError::Bad(format!("no session with {}", query.peer)))?;
let unavailable = |reason: String| TcpAnswer {
available: false,
reason: Some(reason),
connection: None,
};
let answer =
match bgp_fib::sockdiag::tcp_connection(session.local_addr, session.peer.addr).await {
Ok(Some(c)) => TcpAnswer {
available: true,
reason: None,
connection: Some(TcpView::from(&c)),
},
Ok(None) => unavailable(if cfg!(target_os = "linux") {
"the kernel has no such connection".to_string()
} else {
"only on Linux".to_string()
}),
Err(e) => unavailable(format!("the kernel could not be asked: {e}")),
};
Ok(Json(answer))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
@@ -1580,156 +1760,34 @@ mod tests {
.map(|(name, value)| (name.to_string(), value.to_string())); .map(|(name, value)| (name.to_string(), value.to_string()));
assert_eq!(other, expected); assert_eq!(other, expected);
} }
}
/// `GET /api/tcp?peer=ADDRESS:PORT`: what the kernel knows of the TCP /// The kernel's microseconds are shown as milliseconds, and the time
/// connection of the session with that peer, as `/api/peers` names it. /// held up as a share of the busy time; a connection that was never
#[derive(Debug, Deserialize)] /// busy has a share of zero, not of NaN.
pub struct TcpQuery { #[test]
pub peer: String, fn a_tcp_view_has_milliseconds_and_shares_of_the_busy_time() {
} let idle = TcpConnection {
rtt: 1500,
/// A session's TCP connection, as the kernel reports it (Linux only). busy: 0,
/// Times in milliseconds, sizes in octets, rates in octets per second. limited_by_receive_window: 0,
#[derive(Debug, Serialize)] ..TcpConnection::default()
pub struct TcpView {
pub state: &'static str,
pub congestion: Option<String>,
pub rtt_ms: f64,
pub rtt_variance_ms: f64,
pub min_rtt_ms: f64,
pub rto_ms: f64,
pub mss: u32,
pub pmtu: u32,
pub cwnd_segments: u32,
pub ssthresh_segments: Option<u32>,
/// The peer's receive window: what it lets us send.
pub send_window: u32,
/// Our receive window: what we let the peer send.
pub receive_window: u32,
pub receive_space: u32,
pub send_window_scale: u8,
pub receive_window_scale: u8,
/// Octets written by us and not yet acknowledged, and of those the
/// octets not even sent yet.
pub send_queue: u32,
pub not_sent: u32,
pub recv_queue: u32,
pub unacked_segments: u32,
pub retrans_segments: u32,
pub lost_segments: u32,
pub total_retrans: u32,
pub bytes_retrans: u64,
pub pacing_rate: u64,
pub delivery_rate: u64,
pub bytes_sent: u64,
pub bytes_acked: u64,
pub bytes_received: u64,
pub segments_out: u32,
pub segments_in: u32,
/// How long the connection had something to send, and of that how
/// long it was held up by the peer's window and by our send buffer.
pub busy_ms: f64,
pub limited_by_receive_window_ms: f64,
pub limited_by_send_buffer_ms: f64,
/// The same two, as shares of the busy time, 0 to 1.
pub limited_by_receive_window_share: f64,
pub limited_by_send_buffer_share: f64,
pub last_data_sent_ms: u32,
pub last_data_received_ms: u32,
pub last_ack_received_ms: u32,
}
/// The answer to `/api/tcp`.
#[derive(Debug, Serialize)]
pub struct TcpAnswer {
/// Whether the kernel could be asked and knew the connection.
pub available: bool,
/// Why not, if not.
pub reason: Option<String>,
pub connection: Option<TcpView>,
}
pub async fn tcp(
State(state): State<AppState>,
Query(query): Query<TcpQuery>,
) -> Result<Json<TcpAnswer>, QueryError> {
let session = state
.registry
.list()
.into_iter()
.find(|s| socket_text(s.peer.addr) == query.peer)
.ok_or_else(|| QueryError::Bad(format!("no session with {}", query.peer)))?;
let answer =
match bgp_fib::sockdiag::tcp_connection(session.local_addr, session.peer.addr).await {
Ok(Some(c)) => {
let ms = |us: u64| us as f64 / 1000.0;
let share = |part: u64| {
if c.busy == 0 {
0.0
} else {
part as f64 / c.busy as f64
}
}; };
TcpAnswer { let view = TcpView::from(&idle);
available: true, assert_eq!(view.rtt_ms, 1.5);
reason: None, assert_eq!(view.busy_ms, 0.0);
connection: Some(TcpView { assert_eq!(view.limited_by_receive_window_share, 0.0);
state: c.state_name(), assert_eq!(view.limited_by_send_buffer_share, 0.0);
congestion: c.congestion.clone(),
rtt_ms: ms(c.rtt.into()), let busy = TcpConnection {
rtt_variance_ms: ms(c.rtt_variance.into()), busy: 4_000_000,
min_rtt_ms: ms(c.min_rtt.into()), limited_by_receive_window: 1_000_000,
rto_ms: ms(c.rto.into()), limited_by_send_buffer: 3_000_000,
mss: c.mss, ..TcpConnection::default()
pmtu: c.pmtu,
cwnd_segments: c.cwnd,
ssthresh_segments: c.ssthresh,
send_window: c.send_window,
receive_window: c.receive_window,
receive_space: c.receive_space,
send_window_scale: c.send_window_scale,
receive_window_scale: c.receive_window_scale,
send_queue: c.send_queue,
not_sent: c.not_sent,
recv_queue: c.recv_queue,
unacked_segments: c.unacked,
retrans_segments: c.retrans,
lost_segments: c.lost,
total_retrans: c.total_retrans,
bytes_retrans: c.bytes_retrans,
pacing_rate: c.pacing_rate,
delivery_rate: c.delivery_rate,
bytes_sent: c.bytes_sent,
bytes_acked: c.bytes_acked,
bytes_received: c.bytes_received,
segments_out: c.segments_out,
segments_in: c.segments_in,
busy_ms: ms(c.busy),
limited_by_receive_window_ms: ms(c.limited_by_receive_window),
limited_by_send_buffer_ms: ms(c.limited_by_send_buffer),
limited_by_receive_window_share: share(c.limited_by_receive_window),
limited_by_send_buffer_share: share(c.limited_by_send_buffer),
last_data_sent_ms: c.last_data_sent_ms,
last_data_received_ms: c.last_data_received_ms,
last_ack_received_ms: c.last_ack_received_ms,
}),
}
}
Ok(None) => TcpAnswer {
available: false,
reason: Some(if cfg!(target_os = "linux") {
"the kernel has no such connection".to_string()
} else {
"only on Linux".to_string()
}),
connection: None,
},
Err(e) => TcpAnswer {
available: false,
reason: Some(format!("the kernel could not be asked: {e}")),
connection: None,
},
}; };
Ok(Json(answer)) let view = TcpView::from(&busy);
assert_eq!(view.busy_ms, 4000.0);
assert_eq!(view.limited_by_receive_window_ms, 1000.0);
assert_eq!(view.limited_by_receive_window_share, 0.25);
assert_eq!(view.limited_by_send_buffer_share, 0.75);
}
} }
+2 -7
View File
@@ -28,11 +28,6 @@ use serde::Serialize;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tracing::info; use tracing::info;
pub use api::{
AttrSummary, ConfigView, KernelView, LocalRouteView, PeerView, ReceivedRouteView,
RejectedRouteView, RejectionsView, RoutesPage, RpkiView, StatusView,
};
/// Static facts about the router shown in status output. /// Static facts about the router shown in status output.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RouterInfo { pub struct RouterInfo {
@@ -98,8 +93,8 @@ impl RouterInfo {
} }
} }
/// Best-effort host name without pulling in a dependency: ask the `hostname` /// The machine's host name as the kernel has it, for display; a machine
/// utility, which exists on every platform we run on. /// without one, or with one that is not text, is "unknown host".
fn hostname() -> String { fn hostname() -> String {
nix::unistd::gethostname() nix::unistd::gethostname()
.ok() .ok()
+4 -65
View File
@@ -63,70 +63,9 @@ async fn sample_tcp(state: &AppState) {
let label = session.metric_label(); let label = session.metric_label();
let connection = let connection =
bgp_fib::sockdiag::tcp_connection(session.local_addr, session.peer.addr).await; bgp_fib::sockdiag::tcp_connection(session.local_addr, session.peer.addr).await;
let Ok(Some(c)) = connection else { match connection {
tcp.remove(&label); Ok(Some(c)) => tcp.set(&label, &c),
continue; _ => tcp.remove(&label),
}; }
let seconds = |us: u64| us as f64 / 1e6;
let int = |n: u64| i64::try_from(n).unwrap_or(i64::MAX);
let label = [label.as_str()];
tcp.rtt_seconds
.with_label_values(&label)
.set(seconds(c.rtt.into()));
tcp.min_rtt_seconds
.with_label_values(&label)
.set(seconds(c.min_rtt.into()));
tcp.rtt_variance_seconds
.with_label_values(&label)
.set(seconds(c.rtt_variance.into()));
tcp.cwnd_segments
.with_label_values(&label)
.set(c.cwnd.into());
tcp.mss_bytes.with_label_values(&label).set(c.mss.into());
tcp.send_window_bytes
.with_label_values(&label)
.set(c.send_window.into());
tcp.receive_window_bytes
.with_label_values(&label)
.set(c.receive_window.into());
tcp.send_queue_bytes
.with_label_values(&label)
.set(c.send_queue.into());
tcp.not_sent_bytes
.with_label_values(&label)
.set(c.not_sent.into());
tcp.receive_queue_bytes
.with_label_values(&label)
.set(c.recv_queue.into());
tcp.unacked_segments
.with_label_values(&label)
.set(c.unacked.into());
tcp.retransmitted_segments
.with_label_values(&label)
.set(c.total_retrans.into());
tcp.lost_segments
.with_label_values(&label)
.set(c.lost.into());
tcp.busy_seconds
.with_label_values(&label)
.set(seconds(c.busy));
tcp.receive_window_limited_seconds
.with_label_values(&label)
.set(seconds(c.limited_by_receive_window));
tcp.send_buffer_limited_seconds
.with_label_values(&label)
.set(seconds(c.limited_by_send_buffer));
tcp.sent_bytes
.with_label_values(&label)
.set(int(c.bytes_sent));
tcp.received_bytes
.with_label_values(&label)
.set(int(c.bytes_received));
tcp.delivery_rate_bytes_per_second
.with_label_values(&label)
.set(int(c.delivery_rate));
tcp.pacing_rate_bytes_per_second
.with_label_values(&label)
.set(int(c.pacing_rate));
} }
} }
+42 -28
View File
@@ -68,12 +68,15 @@ table.data th { background: var(--head); font-weight: bold; position: sticky; to
table.data > tbody > tr:hover > td { background: var(--hover); } table.data > tbody > tr:hover > td { background: var(--hover); }
table.data td.num, table.data th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; } table.data td.num, table.data th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
table.data tr.group > td { background: var(--band); font-weight: bold; padding: 3px 6px; } table.data tr.group > td { background: var(--band); font-weight: bold; padding: 3px 6px; }
table.data tr.tcp-row > td { background: var(--band); padding: 6px 10px 8px 28px; } /* A row of detail under a row of the table: a session's TCP connection, a
route in full. The route's is indented past the rank column. */
table.data tr.detail > td { background: var(--band); padding: 6px 10px 8px 28px; }
table.data tr.route-detail > td { padding-left: 40px; }
table.data td.attrs { cursor: pointer; } table.data td.attrs { cursor: pointer; }
table.data td.attrs:hover { background: var(--hover); } table.data td.attrs:hover { background: var(--hover); }
table.data tr.route-detail > td { background: var(--band); padding: 6px 10px 8px 40px; } /* Names and values, two columns, as the detail rows show them. */
.tcp-grid { display: grid; grid-template-columns: max-content 1fr; gap: 1px 16px; font-size: 12px; } .kv-grid { display: grid; grid-template-columns: max-content 1fr; gap: 1px 16px; font-size: 12px; }
.tcp-grid > div:nth-child(odd) { color: var(--fg-soft); white-space: nowrap; } .kv-grid > div:nth-child(odd) { color: var(--fg-soft); white-space: nowrap; }
a.tcp-link { color: var(--dim); font-size: 11.5px; text-decoration: none; border: 1px solid var(--line); border-radius: 3px; padding: 0 4px; margin-left: 4px; } a.tcp-link { color: var(--dim); font-size: 11.5px; text-decoration: none; border: 1px solid var(--line); border-radius: 3px; padding: 0 4px; margin-left: 4px; }
a.tcp-link:hover, a.tcp-link.open { color: var(--link); border-color: var(--link); } a.tcp-link:hover, a.tcp-link.open { color: var(--link); border-color: var(--link); }
table.data tr.best > td { background: var(--best-bg); } table.data tr.best > td { background: var(--best-bg); }
@@ -276,6 +279,17 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
if (state.status) $('s-uptime').textContent = fmtDur(t - state.status.started_at_unix); if (state.status) $('s-uptime').textContent = fmtDur(t - state.status.started_at_unix);
} }
const fmtWhen = unix => new Date(unix * 1000).toLocaleString(); const fmtWhen = unix => new Date(unix * 1000).toLocaleString();
const fmtOctets = n => n >= 1048576 ? `${(n / 1048576).toFixed(1)} MiB` : n >= 1024 ? `${(n / 1024).toFixed(1)} KiB` : `${n} B`;
const fmtBits = n => { const b = n * 8; return b >= 1e9 ? `${(b / 1e9).toFixed(2)} Gbit/s` : b >= 1e6 ? `${(b / 1e6).toFixed(1)} Mbit/s` : `${(b / 1e3).toFixed(0)} kbit/s`; };
const fmtMs = n => n >= 1000 ? `${(n / 1000).toFixed(1)} s` : `${n.toFixed(n < 10 ? 2 : 0)} ms`;
const ago = ms => ms >= 60000 ? `${Math.round(ms / 60000)}m` : ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
// A cell across every column of the table that `body` is the body of,
// and a row of one: for a heading, a note or a detail row. The width is
// the table's header's, so a column added there is covered here.
const fullCell = (body, html, cls = '') => `<td colspan="${body.closest('table').querySelectorAll('thead th').length}"${cls ? ` class="${cls}"` : ''}>${html}</td>`;
const fullRow = (body, html, rowClass = '', cellClass = '') => `<tr${rowClass ? ` class="${rowClass}"` : ''}>${fullCell(body, html, cellClass)}</tr>`;
// Names and values in two columns, for a detail row.
const kvGrid = rows => `<div class="kv-grid mono">${rows.map(([k, v]) => `<div>${k}</div><div>${v}</div>`).join('')}</div>`;
// What a community of ours asks for where control communities are on // What a community of ours asks for where control communities are on
// (control_communities), or ''. They are acted on where a route leaves // (control_communities), or ''. They are acted on where a route leaves
@@ -498,8 +512,10 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
'route server': ['RS', 'route server of an Internet exchange'], 'route server': ['RS', 'route server of an Internet exchange'],
'route server client': ['RSC', 'client of a route server we run'], 'route server client': ['RSC', 'client of a route server we run'],
}; };
// A router of our own AS, by its AS number.
const isIbgp = p => p.peer_asn === (state.status || {}).local_asn;
function roleGlyph(p) { function roleGlyph(p) {
if (p.peer_asn === (state.status || {}).local_asn) return glyph('iBGP', 'a router of our own AS'); if (isIbgp(p)) return glyph('iBGP', 'a router of our own AS');
const r = ROLE_GLYPH[p.role]; const r = ROLE_GLYPH[p.role];
if (!r) return glyph('?', 'no role configured', 'amber'); if (!r) return glyph('?', 'no role configured', 'amber');
const confirmed = p.role_confirmed ? '; the peer says so too (RFC 9234)' : p.state === 'Established' ? '; by our configuration, the peer has not said what it is' : ''; const confirmed = p.role_confirmed ? '; the peer says so too (RFC 9234)' : p.state === 'Established' ? '; by our configuration, the peer has not said what it is' : '';
@@ -531,7 +547,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
} }
const ROLE_ORDER = ['iBGP', 'provider', 'route server', 'peer', 'customer', 'route server client', 'none']; const ROLE_ORDER = ['iBGP', 'provider', 'route server', 'peer', 'customer', 'route server client', 'none'];
function peerGroup(p) { function peerGroup(p) {
if (p.peer_asn === (state.status || {}).local_asn) return 'iBGP'; if (isIbgp(p)) return 'iBGP';
return p.role || 'none'; return p.role || 'none';
} }
const GROUP_TITLE = { iBGP: 'Our own routers', provider: 'Providers', 'route server': 'Route servers', peer: 'Peers', customer: 'Customers', 'route server client': 'Route server clients', none: 'Without a role' }; const GROUP_TITLE = { iBGP: 'Our own routers', provider: 'Providers', 'route server': 'Route servers', peer: 'Peers', customer: 'Customers', 'route server client': 'Route server clients', none: 'Without a role' };
@@ -542,7 +558,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const down = peers.filter(p => peerRank(p) === 0).length; const down = peers.filter(p => peerRank(p) === 0).length;
const waiting = peers.filter(p => p.state === 'Passive').length; const waiting = peers.filter(p => p.state === 'Passive').length;
$('peers-count').textContent = `${up} of ${peers.length} established` + (down ? `, ${down} down` : '') + (waiting ? `, ${waiting} waiting` : ''); $('peers-count').textContent = `${up} of ${peers.length} established` + (down ? `, ${down} down` : '') + (waiting ? `, ${waiting} waiting` : '');
if (!peers.length) { setHTML(b, '<tr><td colspan="10" class="dim">No peers configured and none connected.</td></tr>'); return; } if (!peers.length) { setHTML(b, fullRow(b, 'No peers configured and none connected.', '', 'dim')); return; }
const problemsOnly = $('peers-problems').checked; const problemsOnly = $('peers-problems').checked;
// The IPv4 and IPv6 sessions of one neighbour, named <stem>-v4 and // The IPv4 and IPv6 sessions of one neighbour, named <stem>-v4 and
// <stem>-v6, are one neighbour to the reader: listed together, IPv6 // <stem>-v6, are one neighbour to the reader: listed together, IPv6
@@ -585,16 +601,16 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
// they are to us. // they are to us.
const problems = peers.filter(p => peerRank(p) <= 1).sort(byName); const problems = peers.filter(p => peerRank(p) <= 1).sort(byName);
if (problems.length) { if (problems.length) {
html += `<tr class="group"><td colspan="10"><span class="red">Needing attention</span> (${problems.length})</td></tr>` + rows(problems); html += fullRow(b, `<span class="red">Needing attention</span> (${problems.length})`, 'group') + rows(problems);
} }
if (!problemsOnly) { if (!problemsOnly) {
for (const group of ROLE_ORDER) { for (const group of ROLE_ORDER) {
const members = peers.filter(p => peerRank(p) >= 2 && peerGroup(p) === group).sort(byName); const members = peers.filter(p => peerRank(p) >= 2 && peerGroup(p) === group).sort(byName);
if (!members.length) continue; if (!members.length) continue;
html += `<tr class="group"><td colspan="10">${GROUP_TITLE[group]} (${members.length})</td></tr>` + rows(members); html += fullRow(b, `${GROUP_TITLE[group]} (${members.length})`, 'group') + rows(members);
} }
} else if (!problems.length) { } else if (!problems.length) {
html += '<tr><td colspan="10" class="dim">Nothing needs attention.</td></tr>'; html += fullRow(b, 'Nothing needs attention.', '', 'dim');
} }
setHTML(b, html); setHTML(b, html);
renderTcpRows(); renderTcpRows();
@@ -610,10 +626,6 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const open = state.tcpOpen.has(p.peer_addr); const open = state.tcpOpen.has(p.peer_addr);
return `<a href="#" class="tcp-link${open ? ' open' : ''}" data-peer="${esc(p.peer_addr)}" title="TCP_INFO for this session's socket">tcp</a>`; return `<a href="#" class="tcp-link${open ? ' open' : ''}" data-peer="${esc(p.peer_addr)}" title="TCP_INFO for this session's socket">tcp</a>`;
} }
const fmtOctets = n => n >= 1048576 ? `${(n / 1048576).toFixed(1)} MiB` : n >= 1024 ? `${(n / 1024).toFixed(1)} KiB` : `${n} B`;
const fmtBits = n => { const b = n * 8; return b >= 1e9 ? `${(b / 1e9).toFixed(2)} Gbit/s` : b >= 1e6 ? `${(b / 1e6).toFixed(1)} Mbit/s` : `${(b / 1e3).toFixed(0)} kbit/s`; };
const fmtMs = n => n >= 1000 ? `${(n / 1000).toFixed(1)} s` : `${n.toFixed(n < 10 ? 2 : 0)} ms`;
const ago = ms => ms >= 60000 ? `${Math.round(ms / 60000)}m` : ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
function tcpDetail(a) { function tcpDetail(a) {
if (!a.available) return `<span class="dim">${esc(a.reason || 'n/a')}</span>`; if (!a.available) return `<span class="dim">${esc(a.reason || 'n/a')}</span>`;
const c = a.connection; const c = a.connection;
@@ -634,24 +646,25 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
['bytes', `${kv('sent', fmtOctets(c.bytes_sent))} ${kv('acked', fmtOctets(c.bytes_acked))} ${kv('received', fmtOctets(c.bytes_received))} ${kv('segs', `${fmtCount(c.segments_out)}↑ ${fmtCount(c.segments_in)}↓`)}`], ['bytes', `${kv('sent', fmtOctets(c.bytes_sent))} ${kv('acked', fmtOctets(c.bytes_acked))} ${kv('received', fmtOctets(c.bytes_received))} ${kv('segs', `${fmtCount(c.segments_out)}↑ ${fmtCount(c.segments_in)}↓`)}`],
['last', `${kv('snd', ago(c.last_data_sent_ms))} ${kv('rcv', ago(c.last_data_received_ms))} ${kv('ack', ago(c.last_ack_received_ms))}`], ['last', `${kv('snd', ago(c.last_data_sent_ms))} ${kv('rcv', ago(c.last_data_received_ms))} ${kv('ack', ago(c.last_ack_received_ms))}`],
]; ];
return `<div class="tcp-grid mono">${rows.map(([k, v]) => `<div>${k}</div><div>${v}</div>`).join('')}</div>`; return kvGrid(rows);
} }
async function renderTcpRows() { async function renderTcpRows() {
const b = $('peers-body');
for (const addr of state.tcpOpen) { for (const addr of state.tcpOpen) {
const tr = document.querySelector(`#peers-body tr[data-peer-addr="${CSS.escape(addr)}"]`); const tr = b.querySelector(`tr[data-peer-addr="${CSS.escape(addr)}"]`);
if (!tr) continue; if (!tr) continue;
let row = tr.nextElementSibling; let row = tr.nextElementSibling;
if (!row || !row.classList.contains('tcp-row')) { if (!row || !row.classList.contains('tcp-row')) {
row = document.createElement('tr'); row = document.createElement('tr');
row.className = 'tcp-row'; row.className = 'detail tcp-row';
row.innerHTML = '<td colspan="10"><span class="dim">asking the kernel…</span></td>'; row.innerHTML = fullCell(b, '<span class="dim">asking the kernel…</span>');
tr.after(row); tr.after(row);
} }
try { try {
const answer = await getJSON(`/api/tcp?peer=${encodeURIComponent(addr)}`); const answer = await getJSON(`/api/tcp?peer=${encodeURIComponent(addr)}`);
row.innerHTML = `<td colspan="10">${tcpDetail(answer)}</td>`; row.innerHTML = fullCell(b, tcpDetail(answer));
} catch (e) { } catch (e) {
row.innerHTML = `<td colspan="10"><span class="red">${esc(String(e))}</span></td>`; row.innerHTML = fullCell(b, `<span class="red">${esc(String(e))}</span>`);
} }
} }
} }
@@ -790,7 +803,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
function renderLocal() { function renderLocal() {
const b = $('local-body'); $('local-count').textContent = state.local.length; const b = $('local-body'); $('local-count').textContent = state.local.length;
if (!state.local.length) { setHTML(b, '<tr><td colspan="7" class="dim">This router originates no routes.</td></tr>'); return; } if (!state.local.length) { setHTML(b, fullRow(b, 'This router originates no routes.', '', 'dim')); return; }
const local = state.status ? state.status.local_asn : '?'; const local = state.status ? state.status.local_asn : '?';
setHTML(b, state.local.map(r => `<tr> setHTML(b, state.local.map(r => `<tr>
<td class="mono">${esc(r.prefix)}</td><td>${esc(r.origin)}</td> <td class="mono">${esc(r.prefix)}</td><td>${esc(r.origin)}</td>
@@ -826,7 +839,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
: `${shown}${st ? `, for ${fmtCount(st.received_prefixes)} prefixes` : ''}`; : `${shown}${st ? `, for ${fmtCount(st.received_prefixes)} prefixes` : ''}`;
renderPager(); renderPager();
if (!state.received.length) { if (!state.received.length) {
setHTML(b, `<tr><td colspan="12" class="dim">${looking ? 'No route matches.' : 'No routes received from peers.'}</td></tr>`); return; setHTML(b, fullRow(b, looking ? 'No route matches.' : 'No routes received from peers.', '', 'dim')); return;
} }
setHTML(b, state.received.map(r => { const s = r.summary; return `<tr class="${r.best ? 'best' : ''}" data-route="${esc(routeKey(r))}"> setHTML(b, state.received.map(r => { const s = r.summary; return `<tr class="${r.best ? 'best' : ''}" data-route="${esc(routeKey(r))}">
<td class="nowrap">${rankGlyph(r)}${r.stale ? glyph('stale', 'Kept from a session of the peer’s that ended (graceful restart), and not sent again since', 'amber') : ''}</td> <td class="nowrap">${rankGlyph(r)}${r.stale ? glyph('stale', 'Kept from a session of the peer’s that ended (graceful restart), and not sent again since', 'amber') : ''}</td>
@@ -863,18 +876,19 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
if (r.path_id) rows.push(['PATH_ID', `${r.path_id}`]); if (r.path_id) rows.push(['PATH_ID', `${r.path_id}`]);
rows.push(['selection', `${r.best ? 'best' : `rank ${r.rank}`}${r.stale ? ', stale' : ''}`]); rows.push(['selection', `${r.best ? 'best' : `rank ${r.rank}`}${r.stale ? ', stale' : ''}`]);
rows.push(['rpki', r.rpki ? esc(r.rpki) : DASH], ['aspa', r.aspa ? esc(r.aspa) : DASH]); rows.push(['rpki', r.rpki ? esc(r.rpki) : DASH], ['aspa', r.aspa ? esc(r.aspa) : DASH]);
return `<div class="tcp-grid mono">${rows.map(([k, v]) => `<div>${k}</div><div>${v}</div>`).join('')}</div>`; return kvGrid(rows);
} }
function renderRouteDetails() { function renderRouteDetails() {
const b = $('received-body');
for (const key of state.routeOpen) { for (const key of state.routeOpen) {
const r = state.received.find(r => routeKey(r) === key); const r = state.received.find(r => routeKey(r) === key);
const tr = document.querySelector(`#received-body tr[data-route="${CSS.escape(key)}"]`); const tr = b.querySelector(`tr[data-route="${CSS.escape(key)}"]`);
if (!r || !tr) continue; if (!r || !tr) continue;
const next = tr.nextElementSibling; const next = tr.nextElementSibling;
if (next && next.classList.contains('route-detail')) continue; if (next && next.classList.contains('route-detail')) continue;
const row = document.createElement('tr'); const row = document.createElement('tr');
row.className = 'route-detail'; row.className = 'detail route-detail';
row.innerHTML = `<td colspan="12">${routeDetail(r)}</td>`; row.innerHTML = fullCell(b, routeDetail(r));
tr.after(row); tr.after(row);
} }
} }
@@ -961,7 +975,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const total = state.rejected.reduce((n, p) => n + p.total, 0); const total = state.rejected.reduce((n, p) => n + p.total, 0);
const announcements = n => `${fmtCount(n)} announcement${n === 1 ? '' : 's'}`; const announcements = n => `${fmtCount(n)} announcement${n === 1 ? '' : 's'}`;
$('rejected-count').textContent = total ? `${announcements(total)} from ${state.rejected.length} peer${state.rejected.length === 1 ? '' : 's'}${chosen ? `; showing ${chosen}` : ''}` : '0'; $('rejected-count').textContent = total ? `${announcements(total)} from ${state.rejected.length} peer${state.rejected.length === 1 ? '' : 's'}${chosen ? `; showing ${chosen}` : ''}` : '0';
if (!peers.length) { setHTML(b, `<tr><td colspan="11" class="dim">${chosen ? `Nothing ${esc(chosen)} announced has been rejected.` : 'Nothing a peer announced has been rejected.'}</td></tr>`); return; } if (!peers.length) { setHTML(b, fullRow(b, chosen ? `Nothing ${esc(chosen)} announced has been rejected.` : 'Nothing a peer announced has been rejected.', '', 'dim')); return; }
setHTML(b, peers.map(p => { setHTML(b, peers.map(p => {
// Alike: the same prefix, for the same reason, by the same path. // Alike: the same prefix, for the same reason, by the same path.
const alike = new Map(); const alike = new Map();
@@ -974,7 +988,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const lines = [...alike.values()].sort((a, c) => c.at_unix - a.at_unix).slice(0, REJECTED_SHOWN); const lines = [...alike.values()].sort((a, c) => c.at_unix - a.at_unix).slice(0, REJECTED_SHOWN);
const more = alike.size > REJECTED_SHOWN const more = alike.size > REJECTED_SHOWN
? ` <span class="dim">${REJECTED_SHOWN} of ${alike.size} shown; <a href="/api/routes/rejected?peer=${encodeURIComponent(p.peer)}">the latest ${p.recent.length} as json</a>.</span>` : ''; ? ` <span class="dim">${REJECTED_SHOWN} of ${alike.size} shown; <a href="/api/routes/rejected?peer=${encodeURIComponent(p.peer)}">the latest ${p.recent.length} as json</a>.</span>` : '';
return `<tr class="group"><td colspan="11"><a href="#received" class="peer-link" data-peer="${esc(p.peer)}">${esc(p.peer)}</a> <span class="dim" style="font-weight: normal">${esc(p.peer_addr)}</span>: <span style="font-weight: normal">${announcements(p.total)} rejected</span> ${reasonBar(p.by_reason, p.total)}${more}</td></tr>` + return fullRow(b, `<a href="#received" class="peer-link" data-peer="${esc(p.peer)}">${esc(p.peer)}</a> <span class="dim" style="font-weight: normal">${esc(p.peer_addr)}</span>: <span style="font-weight: normal">${announcements(p.total)} rejected</span> ${reasonBar(p.by_reason, p.total)}${more}`, 'group') +
lines.map(r => { const s = r.summary; return `<tr><td></td> lines.map(r => { const s = r.summary; return `<tr><td></td>
<td class="mono">${esc(r.prefix)}${r.path_id ? ` <span class="dim">path ${r.path_id}</span>` : ''}</td> <td class="mono">${esc(r.prefix)}${r.path_id ? ` <span class="dim">path ${r.path_id}</span>` : ''}</td>
<td class="mono path">${clamp(asPath(r.as_path))}</td><td>${esc(r.reason)}</td> <td class="mono path">${clamp(asPath(r.as_path))}</td><td>${esc(r.reason)}</td>
+5 -3
View File
@@ -233,9 +233,11 @@ routes. To return the records for one neighbor, add `?peer=NAME`.
## TCP Connection ## TCP Connection
`/api/tcp?peer=ADDRESS` returns what the kernel reports about the TCP `/api/tcp?peer=PEER` returns what the kernel reports about the TCP
connection of the session with the neighbor at `ADDRESS`, given as connection of the session with the neighbor `PEER`, named as for
`peer_addr` in `/api/peers` shows it: the round-trip time, the congestion `/api/routes/received`: its configured name, or its address, with or
without the port as `peer_addr` in `/api/peers` shows it. The report
covers the round-trip time, the congestion
and receive windows, the queues, the retransmissions, and how long the and receive windows, the queues, the retransmissions, and how long the
connection has been held up by the neighbor's receive window or by the connection has been held up by the neighbor's receive window or by the
local send buffer. Linux only; on other systems, and for a session the local send buffer. Linux only; on other systems, and for a session the