policy, fib: blackhole routing for routes with the BLACKHOLE community
Before this change, a route carrying the BLACKHOLE community (65535:666,
RFC 7999) from an EBGP neighbor was always rejected with the reason
"blackhole request", and nothing discarded traffic. A customer under a
denial-of-service attack had no way to ask this router to drop traffic
to an address.
This change adds `accept_blackholes` to the import policy. When it is
set, a route with the BLACKHOLE community is accepted if:
* its prefix is a single address, a /32 or a /128 (otherwise it is
rejected with the new reason "blackhole for more than one address");
the `max_ipv4_prefix_length` and `max_ipv6_prefix_length` limits do
not apply, as RFC 7999 section 3.2 recommends;
* the address is inside a prefix the neighbor may announce, if
`prefixes` is configured, regardless of the entry's `max_length`
(new `PrefixSet::covers`); without a prefix list, an address inside
the local AS's own prefixes is rejected as before ("prefix is our
own");
* the AS path, origin and RPKI checks that apply to every route pass.
The next hop of an accepted blackhole route is not checked for
third-party addresses and is not resolved, because traffic is never
forwarded to it. The route takes part in best path selection as usual;
a host route is normally the active route for its address.
The kernel task installs the active route as a kernel blackhole route
(`RTN_BLACKHOLE`, shown by `ip route` as `blackhole PREFIX proto bgp`).
Blackhole routes left by an earlier run are adopted at startup like
other routes of ours. The route is advertised to other neighbors as the
export policy and export limits permit, with the community intact, so a
provider that honors BLACKHOLE can discard the traffic upstream.
Implementation:
* `plan::Target` becomes an enum: `Via { gateway, interface }` or
`Discard`. `plan::resolve` takes a `blackhole` flag and returns
`Discard` for such routes without looking at the next hop (the
own-network check still applies). No nexthop object is made for a
discard target.
* `AttrSet::is_blackhole` reads the community off the stored attribute
bytes, like `next_hop` and `as_path`, so the kernel task, the
revalidator and the RPKI revalidation can ask it of every route.
* `ImportPolicy::is_blackhole_request` and `check_blackhole_prefix`
carry the import side; the import flow (`routes.rs`) computes once per
UPDATE whether its routes are an accepted blackhole request and skips
the next hop checks for them.
* `Status::blackholes` and the `blackholes` field of `/api/kernel` count
the installed blackhole routes; the status page shows the count next
to the installed routes.
Not covered: a prefix this router originates with the BLACKHOLE
community is advertised but not installed as a blackhole route. The
documentation says to add a kernel blackhole route by hand for that
case.
Documentation: the "Blackhole Routes" section of the import policy
guide replaces "Blackhole Requests", the kernel guide gains a "Blackhole
Routes" section, the configuration reference describes
`accept_blackholes`, the HTTP API reference describes `blackholes`, and
the RFC 7999 rows of the standards reference are updated.
Testing: unit tests for the attribute check, the prefix checks and the
plan; integration tests that a blackhole request is refused by default
and, with `accept_blackholes`, taken for one address inside the peer's
space and refused otherwise, with the recorded reasons; a kernel test
that a blackhole request becomes a kernel blackhole route whatever its
next hop, is removed on withdrawal and is adopted after a restart. The
existing kernel tests, the interoperability lab and the workspace tests
pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
01ac3e2cd8
commit
6d8f89e509
21 files changed
+673
-87
No files matched your search
+45
-14
@@ -24,7 +24,8 @@ use tracing::{debug, info, warn};
|
|||||||
|
|
||||||
use crate::interfaces::Interfaces;
|
use crate::interfaces::Interfaces;
|
||||||
use crate::netlink::{
|
use crate::netlink::{
|
||||||
self, Change, Dump, KIND_UNICAST, Message, PROTOCOL_BGP, Route, decode, group,
|
self, Change, Dump, KIND_BLACKHOLE, KIND_UNICAST, KIND_UNSPEC, Message, PROTOCOL_BGP, Route,
|
||||||
|
decode, group,
|
||||||
};
|
};
|
||||||
use crate::plan::{
|
use crate::plan::{
|
||||||
HashMap as FastHashMap, Neighbor, Op, Plan, Target, Unresolved, resolve_next_hop,
|
HashMap as FastHashMap, Neighbor, Op, Plan, Target, Unresolved, resolve_next_hop,
|
||||||
@@ -444,6 +445,8 @@ enum News {
|
|||||||
prefix: IpPrefix,
|
prefix: IpPrefix,
|
||||||
peer: PeerIndex,
|
peer: PeerIndex,
|
||||||
next_hop: Option<NlriNextHop>,
|
next_hop: Option<NlriNextHop>,
|
||||||
|
/// Whether the route asks for its packets to be dropped.
|
||||||
|
blackhole: bool,
|
||||||
},
|
},
|
||||||
Withdrawn(IpPrefix),
|
Withdrawn(IpPrefix),
|
||||||
Synced,
|
Synced,
|
||||||
@@ -539,7 +542,7 @@ impl Task {
|
|||||||
&& let (Some(gateway), Some(interface)) =
|
&& let (Some(gateway), Some(interface)) =
|
||||||
(nexthop.gateway, nexthop.interface)
|
(nexthop.gateway, nexthop.interface)
|
||||||
{
|
{
|
||||||
by_id.insert(nexthop.id, Target { gateway, interface });
|
by_id.insert(nexthop.id, Target::Via { gateway, interface });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -642,8 +645,9 @@ impl Task {
|
|||||||
nexthops.get(&id).copied()
|
nexthops.get(&id).copied()
|
||||||
}
|
}
|
||||||
(None, Some(gateway), Some(interface)) if route.kind == KIND_UNICAST => {
|
(None, Some(gateway), Some(interface)) if route.kind == KIND_UNICAST => {
|
||||||
Some(Target { gateway, interface })
|
Some(Target::Via { gateway, interface })
|
||||||
}
|
}
|
||||||
|
(None, None, _) if route.kind == KIND_BLACKHOLE => Some(Target::Discard),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
self.plan.adopt(route.prefix, target, keep);
|
self.plan.adopt(route.prefix, target, keep);
|
||||||
@@ -661,6 +665,9 @@ impl Task {
|
|||||||
if let Some(id) = known.get(&target) {
|
if let Some(id) = known.get(&target) {
|
||||||
return Ok(Ok(*id));
|
return Ok(Ok(*id));
|
||||||
}
|
}
|
||||||
|
let Target::Via { gateway, interface } = target else {
|
||||||
|
unreachable!("a route that discards names no nexthop object");
|
||||||
|
};
|
||||||
// One at a time, and waited for: there is one for each neighbor,
|
// One at a time, and waited for: there is one for each neighbor,
|
||||||
// not for each route, and a route must not name one that is not
|
// not for each route, and a route must not name one that is not
|
||||||
// there yet.
|
// there yet.
|
||||||
@@ -669,7 +676,7 @@ impl Task {
|
|||||||
.control
|
.control
|
||||||
.ask(
|
.ask(
|
||||||
|out, sequence| {
|
|out, sequence| {
|
||||||
netlink::nexthop_request(out, sequence, &target.gateway, target.interface);
|
netlink::nexthop_request(out, sequence, &gateway, interface);
|
||||||
},
|
},
|
||||||
|message| {
|
|message| {
|
||||||
if let Message::Nexthop {
|
if let Message::Nexthop {
|
||||||
@@ -684,7 +691,7 @@ impl Task {
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(match (errno, made) {
|
Ok(match (errno, made) {
|
||||||
(0, Some(id)) => {
|
(0, Some(id)) => {
|
||||||
debug!(id, gateway = %target.gateway, "Made a nexthop object");
|
debug!(id, %gateway, "Made a nexthop object");
|
||||||
self.owned.insert(id);
|
self.owned.insert(id);
|
||||||
if let Some(known) = &mut self.nexthops {
|
if let Some(known) = &mut self.nexthops {
|
||||||
known.insert(target, id);
|
known.insert(target, id);
|
||||||
@@ -711,16 +718,29 @@ impl Task {
|
|||||||
kind: KIND_UNICAST,
|
kind: KIND_UNICAST,
|
||||||
};
|
};
|
||||||
let change = match op {
|
let change = match op {
|
||||||
Op::Remove { .. } => Change::Remove,
|
// Whatever kind of route of ours there is for the prefix: the
|
||||||
|
// kernel removes a route of the kind asked for only, and a
|
||||||
|
// blackhole is not a unicast route.
|
||||||
|
Op::Remove { .. } => {
|
||||||
|
route.kind = KIND_UNSPEC;
|
||||||
|
Change::Remove
|
||||||
|
}
|
||||||
|
Op::Install {
|
||||||
|
target: Target::Discard,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
route.kind = KIND_BLACKHOLE;
|
||||||
|
Change::Replace
|
||||||
|
}
|
||||||
Op::Install { target, .. } => {
|
Op::Install { target, .. } => {
|
||||||
if self.nexthops.is_some() {
|
if self.nexthops.is_some() {
|
||||||
match self.nexthop(*target).await? {
|
match self.nexthop(*target).await? {
|
||||||
Ok(id) => route.nexthop = Some(id),
|
Ok(id) => route.nexthop = Some(id),
|
||||||
Err(errno) => return Ok(Err(errno)),
|
Err(errno) => return Ok(Err(errno)),
|
||||||
}
|
}
|
||||||
} else {
|
} else if let Target::Via { gateway, interface } = target {
|
||||||
route.gateway = Some(target.gateway);
|
route.gateway = Some(*gateway);
|
||||||
route.interface = Some(target.interface);
|
route.interface = Some(*interface);
|
||||||
}
|
}
|
||||||
Change::Replace
|
Change::Replace
|
||||||
}
|
}
|
||||||
@@ -789,14 +809,20 @@ impl Task {
|
|||||||
}
|
}
|
||||||
let error = io::Error::from_raw_os_error(errno);
|
let error = io::Error::from_raw_os_error(errno);
|
||||||
let what = match op {
|
let what = match op {
|
||||||
Op::Install { prefix, target } => format!(
|
Op::Install {
|
||||||
"install {prefix} via {} on {}",
|
prefix,
|
||||||
target.gateway,
|
target: Target::Via { gateway, interface },
|
||||||
self.topology.interfaces.link(target.interface).map_or_else(
|
} => format!(
|
||||||
|| format!("interface {}", target.interface),
|
"install {prefix} via {gateway} on {}",
|
||||||
|
self.topology.interfaces.link(*interface).map_or_else(
|
||||||
|
|| format!("interface {interface}"),
|
||||||
|link| link.name.clone()
|
|link| link.name.clone()
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
Op::Install {
|
||||||
|
prefix,
|
||||||
|
target: Target::Discard,
|
||||||
|
} => format!("install a blackhole for {prefix}"),
|
||||||
Op::Remove { prefix } => format!("remove {prefix}"),
|
Op::Remove { prefix } => format!("remove {prefix}"),
|
||||||
};
|
};
|
||||||
// Counted always, said out loud once in a while: a table's worth
|
// Counted always, said out loud once in a while: a table's worth
|
||||||
@@ -836,6 +862,7 @@ impl Task {
|
|||||||
prefix,
|
prefix,
|
||||||
peer,
|
peer,
|
||||||
next_hop: attrs.next_hop(),
|
next_hop: attrs.next_hop(),
|
||||||
|
blackhole: attrs.is_blackhole(),
|
||||||
},
|
},
|
||||||
Event::Withdrawn { prefix } => News::Withdrawn(prefix),
|
Event::Withdrawn { prefix } => News::Withdrawn(prefix),
|
||||||
// Not given to a follower of the best routes.
|
// Not given to a follower of the best routes.
|
||||||
@@ -858,6 +885,7 @@ impl Task {
|
|||||||
prefix,
|
prefix,
|
||||||
peer,
|
peer,
|
||||||
next_hop,
|
next_hop,
|
||||||
|
blackhole,
|
||||||
} => {
|
} => {
|
||||||
// Whose link a link-local next hop is on is asked only
|
// Whose link a link-local next hop is on is asked only
|
||||||
// where there is one.
|
// where there is one.
|
||||||
@@ -877,6 +905,8 @@ impl Task {
|
|||||||
// next hop: it is about the prefix.
|
// next hop: it is about the prefix.
|
||||||
let target = if topology.interfaces.has_network_around(&prefix) {
|
let target = if topology.interfaces.has_network_around(&prefix) {
|
||||||
Err(Unresolved::OwnNetwork)
|
Err(Unresolved::OwnNetwork)
|
||||||
|
} else if blackhole {
|
||||||
|
Ok(Target::Discard)
|
||||||
} else {
|
} else {
|
||||||
*self
|
*self
|
||||||
.resolved
|
.resolved
|
||||||
@@ -1075,6 +1105,7 @@ impl Task {
|
|||||||
status.synced = self.synced;
|
status.synced = self.synced;
|
||||||
status.installed = self.plan.installed();
|
status.installed = self.plan.installed();
|
||||||
status.kept = if self.keeping { self.plan.kept() } else { 0 };
|
status.kept = if self.keeping { self.plan.kept() } else { 0 };
|
||||||
|
status.blackholes = self.plan.blackholes();
|
||||||
status.refused = self.plan.refused();
|
status.refused = self.plan.refused();
|
||||||
status.unresolved = self
|
status.unresolved = self
|
||||||
.plan
|
.plan
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ pub struct Status {
|
|||||||
/// How many of them are kept from an earlier run though the routing
|
/// How many of them are kept from an earlier run though the routing
|
||||||
/// table does not have them (yet); see [`FibConfig::stale_time`].
|
/// table does not have them (yet); see [`FibConfig::stale_time`].
|
||||||
pub kept: usize,
|
pub kept: usize,
|
||||||
|
/// How many of them are blackhole routes, which drop the packets for
|
||||||
|
/// their prefix.
|
||||||
|
pub blackholes: usize,
|
||||||
/// Prefixes whose best route is not in the kernel because its next
|
/// Prefixes whose best route is not in the kernel because its next
|
||||||
/// hop is not usable, by why.
|
/// hop is not usable, by why.
|
||||||
pub unresolved: Vec<(&'static str, usize)>,
|
pub unresolved: Vec<(&'static str, usize)>,
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ pub const TABLE_MAIN: u32 = 254;
|
|||||||
|
|
||||||
/// A route that forwards to a gateway or out of an interface
|
/// A route that forwards to a gateway or out of an interface
|
||||||
/// (`RTN_UNICAST`), as opposed to one that drops, say.
|
/// (`RTN_UNICAST`), as opposed to one that drops, say.
|
||||||
|
/// `RTN_UNSPEC`: in a request to remove a route, a route of any kind.
|
||||||
|
pub const KIND_UNSPEC: u8 = 0;
|
||||||
pub const KIND_UNICAST: u8 = 1;
|
pub const KIND_UNICAST: u8 = 1;
|
||||||
|
/// `RTN_BLACKHOLE`: a route that discards the packets for its prefix.
|
||||||
|
pub const KIND_BLACKHOLE: u8 = 6;
|
||||||
|
|
||||||
const HEADER_LEN: usize = 16;
|
const HEADER_LEN: usize = 16;
|
||||||
|
|
||||||
|
|||||||
+123
-40
@@ -18,10 +18,16 @@ pub(crate) type HashSet<K> = std::collections::HashSet<K, foldhash::fast::Random
|
|||||||
|
|
||||||
/// Where the kernel is to send packets for a prefix.
|
/// Where the kernel is to send packets for a prefix.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct Target {
|
pub enum Target {
|
||||||
pub gateway: IpAddr,
|
/// To a gateway, out of an interface.
|
||||||
|
Via {
|
||||||
|
gateway: IpAddr,
|
||||||
/// The index of the interface the gateway is on.
|
/// The index of the interface the gateway is on.
|
||||||
pub interface: u32,
|
interface: u32,
|
||||||
|
},
|
||||||
|
/// Nowhere: the packets are dropped. For a route that asks for that
|
||||||
|
/// with the BLACKHOLE community (RFC 7999).
|
||||||
|
Discard,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Why a route's next hop is not something the kernel can be given.
|
/// Why a route's next hop is not something the kernel can be given.
|
||||||
@@ -105,7 +111,9 @@ impl From<IpAddr> for Neighbor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Works out where the kernel is to send packets for a route to `prefix`
|
/// Works out where the kernel is to send packets for a route to `prefix`
|
||||||
/// with the next hop `next_hop`, learned from `peer`.
|
/// with the next hop `next_hop`, learned from `peer`. `blackhole` says
|
||||||
|
/// the route asks for its packets to be dropped (the BLACKHOLE community,
|
||||||
|
/// RFC 7999): then they go nowhere, and the next hop is not looked at.
|
||||||
///
|
///
|
||||||
/// A next hop that is a neighbor on one of our interfaces is where the
|
/// A next hop that is a neighbor on one of our interfaces is where the
|
||||||
/// packets go. One that is not is looked up in the kernel's own routes,
|
/// packets go. One that is not is looked up in the kernel's own routes,
|
||||||
@@ -118,10 +126,14 @@ pub fn resolve(
|
|||||||
prefix: &IpPrefix,
|
prefix: &IpPrefix,
|
||||||
next_hop: Option<&NlriNextHop>,
|
next_hop: Option<&NlriNextHop>,
|
||||||
peer: Option<Neighbor>,
|
peer: Option<Neighbor>,
|
||||||
|
blackhole: bool,
|
||||||
) -> Result<Target, Unresolved> {
|
) -> Result<Target, Unresolved> {
|
||||||
if topology.interfaces.has_network_around(prefix) {
|
if topology.interfaces.has_network_around(prefix) {
|
||||||
return Err(Unresolved::OwnNetwork);
|
return Err(Unresolved::OwnNetwork);
|
||||||
}
|
}
|
||||||
|
if blackhole {
|
||||||
|
return Ok(Target::Discard);
|
||||||
|
}
|
||||||
resolve_next_hop(topology, prefix.address_family(), next_hop, peer)
|
resolve_next_hop(topology, prefix.address_family(), next_hop, peer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +180,7 @@ fn through_routes(
|
|||||||
if routes == IpVersion::Ipv6 && gateway.is_ipv4() {
|
if routes == IpVersion::Ipv6 && gateway.is_ipv4() {
|
||||||
return Err(Unresolved::NoRoute);
|
return Err(Unresolved::NoRoute);
|
||||||
}
|
}
|
||||||
Ok(Target { gateway, interface })
|
Ok(Target::Via { gateway, interface })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The part of [`resolve`] that is the same for every route of IP version
|
/// The part of [`resolve`] that is the same for every route of IP version
|
||||||
@@ -198,7 +210,7 @@ pub fn resolve_next_hop(
|
|||||||
return Err(Unresolved::Ourselves);
|
return Err(Unresolved::Ourselves);
|
||||||
}
|
}
|
||||||
match interfaces.connected(gateway) {
|
match interfaces.connected(gateway) {
|
||||||
Some(connected) => Ok(Target {
|
Some(connected) => Ok(Target::Via {
|
||||||
gateway,
|
gateway,
|
||||||
interface: connected.interface,
|
interface: connected.interface,
|
||||||
}),
|
}),
|
||||||
@@ -226,7 +238,7 @@ pub fn resolve_next_hop(
|
|||||||
{
|
{
|
||||||
return Err(Unresolved::Ourselves);
|
return Err(Unresolved::Ourselves);
|
||||||
}
|
}
|
||||||
Ok(Target {
|
Ok(Target::Via {
|
||||||
gateway: gateway.into(),
|
gateway: gateway.into(),
|
||||||
interface,
|
interface,
|
||||||
})
|
})
|
||||||
@@ -313,6 +325,14 @@ impl Plan {
|
|||||||
self.installed.len()
|
self.installed.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many of the routes the kernel has drop their packets.
|
||||||
|
pub fn blackholes(&self) -> usize {
|
||||||
|
self.installed
|
||||||
|
.values()
|
||||||
|
.filter(|entry| entry.target == Some(Target::Discard))
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
/// How many prefixes have a best route that could not be given to the
|
/// How many prefixes have a best route that could not be given to the
|
||||||
/// kernel for want of a usable next hop.
|
/// kernel for want of a usable next hop.
|
||||||
pub fn unresolved(&self) -> usize {
|
pub fn unresolved(&self) -> usize {
|
||||||
@@ -507,7 +527,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn target(gateway: &str, interface: u32) -> Target {
|
fn target(gateway: &str, interface: u32) -> Target {
|
||||||
Target {
|
Target::Via {
|
||||||
gateway: gateway.parse().unwrap(),
|
gateway: gateway.parse().unwrap(),
|
||||||
interface,
|
interface,
|
||||||
}
|
}
|
||||||
@@ -575,16 +595,16 @@ mod tests {
|
|||||||
let v6_hop = |s: &str| NlriNextHop::Ipv6(s.parse().unwrap());
|
let v6_hop = |s: &str| NlriNextHop::Ipv6(s.parse().unwrap());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v4, Some(&v4_hop("192.0.2.2")), peer),
|
resolve(&interfaces, &v4, Some(&v4_hop("192.0.2.2")), peer, false),
|
||||||
Ok(target("192.0.2.2", 2))
|
Ok(target("192.0.2.2", 2))
|
||||||
);
|
);
|
||||||
// A neighbor other than the peer: a route server's routes.
|
// A neighbor other than the peer: a route server's routes.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v4, Some(&v4_hop("198.51.100.9")), peer),
|
resolve(&interfaces, &v4, Some(&v4_hop("198.51.100.9")), peer, false),
|
||||||
Ok(target("198.51.100.9", 3))
|
Ok(target("198.51.100.9", 3))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&v6_hop("2001:db8::2")), peer),
|
resolve(&interfaces, &v6, Some(&v6_hop("2001:db8::2")), peer, false),
|
||||||
Ok(target("2001:db8::2", 2))
|
Ok(target("2001:db8::2", 2))
|
||||||
);
|
);
|
||||||
for (hop, why) in [
|
for (hop, why) in [
|
||||||
@@ -592,10 +612,13 @@ mod tests {
|
|||||||
(Some(v4_hop("192.0.2.1")), Unresolved::Ourselves),
|
(Some(v4_hop("192.0.2.1")), Unresolved::Ourselves),
|
||||||
(None, Unresolved::Missing),
|
(None, Unresolved::Missing),
|
||||||
] {
|
] {
|
||||||
assert_eq!(resolve(&interfaces, &v4, hop.as_ref(), peer), Err(why));
|
assert_eq!(
|
||||||
|
resolve(&interfaces, &v4, hop.as_ref(), peer, false),
|
||||||
|
Err(why)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&v4_hop("192.0.2.2")), peer),
|
resolve(&interfaces, &v6, Some(&v4_hop("192.0.2.2")), peer, false),
|
||||||
Err(Unresolved::OtherFamily)
|
Err(Unresolved::OtherFamily)
|
||||||
);
|
);
|
||||||
// A route for part of a network we are on is not for the kernel,
|
// A route for part of a network we are on is not for the kernel,
|
||||||
@@ -612,7 +635,7 @@ mod tests {
|
|||||||
IpVersion::Ipv6 => v6_hop("2001:db8::2"),
|
IpVersion::Ipv6 => v6_hop("2001:db8::2"),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &ours, Some(&hop), peer),
|
resolve(&interfaces, &ours, Some(&hop), peer, false),
|
||||||
Err(Unresolved::OwnNetwork),
|
Err(Unresolved::OwnNetwork),
|
||||||
"{ours}"
|
"{ours}"
|
||||||
);
|
);
|
||||||
@@ -622,14 +645,15 @@ mod tests {
|
|||||||
&interfaces,
|
&interfaces,
|
||||||
&prefix("192.0.0.0/22"),
|
&prefix("192.0.0.0/22"),
|
||||||
Some(&v4_hop("192.0.2.2")),
|
Some(&v4_hop("192.0.2.2")),
|
||||||
peer
|
peer,
|
||||||
|
false
|
||||||
),
|
),
|
||||||
Ok(target("192.0.2.2", 2))
|
Ok(target("192.0.2.2", 2))
|
||||||
);
|
);
|
||||||
// An IPv4 route by way of an IPv6 neighbor: a link with only IPv6
|
// An IPv4 route by way of an IPv6 neighbor: a link with only IPv6
|
||||||
// addresses (RFC 8950).
|
// addresses (RFC 8950).
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v4, Some(&v6_hop("2001:db8::2")), peer),
|
resolve(&interfaces, &v4, Some(&v6_hop("2001:db8::2")), peer, false),
|
||||||
Ok(target("2001:db8::2", 2))
|
Ok(target("2001:db8::2", 2))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -641,6 +665,7 @@ mod tests {
|
|||||||
link_local: "fe80::2".parse().unwrap(),
|
link_local: "fe80::2".parse().unwrap(),
|
||||||
}),
|
}),
|
||||||
Some(neighbor("2001:db8::2")),
|
Some(neighbor("2001:db8::2")),
|
||||||
|
false
|
||||||
),
|
),
|
||||||
Ok(target("fe80::2", 2))
|
Ok(target("fe80::2", 2))
|
||||||
);
|
);
|
||||||
@@ -658,26 +683,44 @@ mod tests {
|
|||||||
};
|
};
|
||||||
// The global address is a neighbor: it is used.
|
// The global address is a neighbor: it is used.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&both("2001:db8::2")), peer),
|
resolve(&interfaces, &v6, Some(&both("2001:db8::2")), peer, false),
|
||||||
Ok(target("2001:db8::2", 2))
|
Ok(target("2001:db8::2", 2))
|
||||||
);
|
);
|
||||||
// It is not: the link-local one, on the interface the peer is on.
|
// It is not: the link-local one, on the interface the peer is on.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&both("2001:db8:ffff::2")), peer),
|
resolve(
|
||||||
|
&interfaces,
|
||||||
|
&v6,
|
||||||
|
Some(&both("2001:db8:ffff::2")),
|
||||||
|
peer,
|
||||||
|
false
|
||||||
|
),
|
||||||
Ok(target("fe80::2", 2))
|
Ok(target("fe80::2", 2))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&NlriNextHop::Ipv6(link_local)), peer),
|
resolve(
|
||||||
|
&interfaces,
|
||||||
|
&v6,
|
||||||
|
Some(&NlriNextHop::Ipv6(link_local)),
|
||||||
|
peer,
|
||||||
|
false
|
||||||
|
),
|
||||||
Ok(target("fe80::2", 2))
|
Ok(target("fe80::2", 2))
|
||||||
);
|
);
|
||||||
// A peer that is not a neighbor says nothing about the link.
|
// A peer that is not a neighbor says nothing about the link.
|
||||||
let far = Some(neighbor("2001:db8:ffff::9"));
|
let far = Some(neighbor("2001:db8:ffff::9"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&NlriNextHop::Ipv6(link_local)), far),
|
resolve(
|
||||||
|
&interfaces,
|
||||||
|
&v6,
|
||||||
|
Some(&NlriNextHop::Ipv6(link_local)),
|
||||||
|
far,
|
||||||
|
false
|
||||||
|
),
|
||||||
Err(Unresolved::NoRoute)
|
Err(Unresolved::NoRoute)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&both("2001:db8::1")), peer),
|
resolve(&interfaces, &v6, Some(&both("2001:db8::1")), peer, false),
|
||||||
Err(Unresolved::Ourselves)
|
Err(Unresolved::Ourselves)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -722,7 +765,7 @@ mod tests {
|
|||||||
for interface in [2, 3] {
|
for interface in [2, 3] {
|
||||||
for route in [&v4, &v6] {
|
for route in [&v4, &v6] {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, route, Some(&next_hop), on(interface)),
|
resolve(&interfaces, route, Some(&next_hop), on(interface), false),
|
||||||
Ok(target("fe80::2", interface)),
|
Ok(target("fe80::2", interface)),
|
||||||
"{next_hop:?} on {interface}"
|
"{next_hop:?} on {interface}"
|
||||||
);
|
);
|
||||||
@@ -730,24 +773,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
// An interface that is not there, or is down.
|
// An interface that is not there, or is down.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&next_hop), on(9)),
|
resolve(&interfaces, &v6, Some(&next_hop), on(9), false),
|
||||||
Err(Unresolved::NoRoute)
|
Err(Unresolved::NoRoute)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Our own address on that link, which on another is a neighbor's.
|
// Our own address on that link, which on another is a neighbor's.
|
||||||
let ours = NlriNextHop::Ipv6("fe80::a".parse().unwrap());
|
let ours = NlriNextHop::Ipv6("fe80::a".parse().unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&ours), on(2)),
|
resolve(&interfaces, &v6, Some(&ours), on(2), false),
|
||||||
Err(Unresolved::Ourselves)
|
Err(Unresolved::Ourselves)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&ours), on(3)),
|
resolve(&interfaces, &v6, Some(&ours), on(3), false),
|
||||||
Ok(target("fe80::a", 3))
|
Ok(target("fe80::a", 3))
|
||||||
);
|
);
|
||||||
// A global next hop on such a session is looked up as any other.
|
// A global next hop on such a session is looked up as any other.
|
||||||
let global = NlriNextHop::Ipv6("2001:db8::2".parse().unwrap());
|
let global = NlriNextHop::Ipv6("2001:db8::2".parse().unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&interfaces, &v6, Some(&global), on(3)),
|
resolve(&interfaces, &v6, Some(&global), on(3), false),
|
||||||
Ok(target("2001:db8::2", 2))
|
Ok(target("2001:db8::2", 2))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -767,7 +810,7 @@ mod tests {
|
|||||||
|
|
||||||
// Nothing says the way.
|
// Nothing says the way.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&far), peer),
|
resolve(&topology, &v4, Some(&far), peer, false),
|
||||||
Err(Unresolved::NoRoute)
|
Err(Unresolved::NoRoute)
|
||||||
);
|
);
|
||||||
// The default route does not count unless asked for.
|
// The default route does not count unless asked for.
|
||||||
@@ -776,12 +819,12 @@ mod tests {
|
|||||||
kernel_route("0.0.0.0/0", Some("192.0.2.254"), Some(2)),
|
kernel_route("0.0.0.0/0", Some("192.0.2.254"), Some(2)),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&far), peer),
|
resolve(&topology, &v4, Some(&far), peer, false),
|
||||||
Err(Unresolved::OnlyDefault)
|
Err(Unresolved::OnlyDefault)
|
||||||
);
|
);
|
||||||
topology.via_default = true;
|
topology.via_default = true;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&far), peer),
|
resolve(&topology, &v4, Some(&far), peer, false),
|
||||||
Ok(target("192.0.2.254", 2))
|
Ok(target("192.0.2.254", 2))
|
||||||
);
|
);
|
||||||
topology.via_default = false;
|
topology.via_default = false;
|
||||||
@@ -791,7 +834,7 @@ mod tests {
|
|||||||
kernel_route("10.9.0.0/16", Some("198.51.100.9"), Some(3)),
|
kernel_route("10.9.0.0/16", Some("198.51.100.9"), Some(3)),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&far), peer),
|
resolve(&topology, &v4, Some(&far), peer, false),
|
||||||
Ok(target("198.51.100.9", 3))
|
Ok(target("198.51.100.9", 3))
|
||||||
);
|
);
|
||||||
// A more specific route onto an interface, as a tunnel's is: the
|
// A more specific route onto an interface, as a tunnel's is: the
|
||||||
@@ -806,7 +849,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
with_route(&mut topology, kernel_route("10.9.9.0/24", None, Some(7)));
|
with_route(&mut topology, kernel_route("10.9.9.0/24", None, Some(7)));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&far), peer),
|
resolve(&topology, &v4, Some(&far), peer, false),
|
||||||
Ok(target("10.9.9.9", 7))
|
Ok(target("10.9.9.9", 7))
|
||||||
);
|
);
|
||||||
// The tunnel goes down: the kernel keeps such routes, we do not
|
// The tunnel goes down: the kernel keeps such routes, we do not
|
||||||
@@ -820,7 +863,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&far), peer),
|
resolve(&topology, &v4, Some(&far), peer, false),
|
||||||
Err(Unresolved::NoRoute)
|
Err(Unresolved::NoRoute)
|
||||||
);
|
);
|
||||||
// A route that names a nexthop object goes where the object says.
|
// A route that names a nexthop object goes where the object says.
|
||||||
@@ -832,7 +875,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&far), peer),
|
resolve(&topology, &v4, Some(&far), peer, false),
|
||||||
Err(Unresolved::NoRoute),
|
Err(Unresolved::NoRoute),
|
||||||
"an object the kernel has not told of"
|
"an object the kernel has not told of"
|
||||||
);
|
);
|
||||||
@@ -846,7 +889,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&far), peer),
|
resolve(&topology, &v4, Some(&far), peer, false),
|
||||||
Ok(target("192.0.2.77", 2))
|
Ok(target("192.0.2.77", 2))
|
||||||
);
|
);
|
||||||
// A route that discards packets is a way to nowhere.
|
// A route that discards packets is a way to nowhere.
|
||||||
@@ -858,7 +901,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&far), peer),
|
resolve(&topology, &v4, Some(&far), peer, false),
|
||||||
Err(Unresolved::Discarded)
|
Err(Unresolved::Discarded)
|
||||||
);
|
);
|
||||||
// Routes of BGP's never say the way, be the prefix ever so long.
|
// Routes of BGP's never say the way, be the prefix ever so long.
|
||||||
@@ -878,7 +921,7 @@ mod tests {
|
|||||||
kernel_route("2001:db8:9::/48", Some("198.51.100.9"), Some(3)),
|
kernel_route("2001:db8:9::/48", Some("198.51.100.9"), Some(3)),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v6, Some(&v6_hop("2001:db8:9::9")), peer),
|
resolve(&topology, &v6, Some(&v6_hop("2001:db8:9::9")), peer, false),
|
||||||
Err(Unresolved::NoRoute)
|
Err(Unresolved::NoRoute)
|
||||||
);
|
);
|
||||||
with_route(
|
with_route(
|
||||||
@@ -886,11 +929,11 @@ mod tests {
|
|||||||
kernel_route("2001:db8:9::/48", Some("2001:db8::9"), Some(2)),
|
kernel_route("2001:db8:9::/48", Some("2001:db8::9"), Some(2)),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v6, Some(&v6_hop("2001:db8:9::9")), peer),
|
resolve(&topology, &v6, Some(&v6_hop("2001:db8:9::9")), peer, false),
|
||||||
Ok(target("2001:db8::9", 2))
|
Ok(target("2001:db8::9", 2))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&v6_hop("2001:db8:9::9")), peer),
|
resolve(&topology, &v4, Some(&v6_hop("2001:db8:9::9")), peer, false),
|
||||||
Ok(target("2001:db8::9", 2))
|
Ok(target("2001:db8::9", 2))
|
||||||
);
|
);
|
||||||
// A neighbor is a neighbor, whatever the routes say.
|
// A neighbor is a neighbor, whatever the routes say.
|
||||||
@@ -899,11 +942,51 @@ mod tests {
|
|||||||
kernel_route("192.0.2.2/32", Some("198.51.100.9"), Some(3)),
|
kernel_route("192.0.2.2/32", Some("198.51.100.9"), Some(3)),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve(&topology, &v4, Some(&v4_hop("192.0.2.2")), peer),
|
resolve(&topology, &v4, Some(&v4_hop("192.0.2.2")), peer, false),
|
||||||
Ok(target("192.0.2.2", 2))
|
Ok(target("192.0.2.2", 2))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A route that asks for its packets to be dropped goes nowhere,
|
||||||
|
/// whatever its next hop; one for a network of ours does not.
|
||||||
|
#[test]
|
||||||
|
fn a_blackhole_route_resolves_to_nowhere() {
|
||||||
|
let topology = interfaces();
|
||||||
|
let far = NlriNextHop::Ipv4("10.9.9.9".parse().unwrap());
|
||||||
|
let peer = Some(neighbor("192.0.2.2"));
|
||||||
|
assert_eq!(
|
||||||
|
resolve(
|
||||||
|
&topology,
|
||||||
|
&prefix("203.0.113.9/32"),
|
||||||
|
Some(&far),
|
||||||
|
peer,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
Err(Unresolved::NoRoute)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve(&topology, &prefix("203.0.113.9/32"), Some(&far), peer, true),
|
||||||
|
Ok(Target::Discard)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve(&topology, &prefix("203.0.113.9/32"), None, peer, true),
|
||||||
|
Ok(Target::Discard)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve(&topology, &prefix("192.0.2.9/32"), Some(&far), peer, true),
|
||||||
|
Err(Unresolved::OwnNetwork)
|
||||||
|
);
|
||||||
|
let mut plan = Plan::default();
|
||||||
|
assert_eq!(
|
||||||
|
plan.best(prefix("203.0.113.9/32"), Ok(Target::Discard)),
|
||||||
|
Some(Op::Install {
|
||||||
|
prefix: prefix("203.0.113.9/32"),
|
||||||
|
target: Target::Discard
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!((plan.installed(), plan.blackholes()), (1, 1));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_kernel_is_asked_only_for_what_changes() {
|
fn the_kernel_is_asked_only_for_what_changes() {
|
||||||
let mut plan = Plan::default();
|
let mut plan = Plan::default();
|
||||||
@@ -1087,7 +1170,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn the_kernel_ends_up_with_the_best_routes_that_resolve(
|
fn the_kernel_ends_up_with_the_best_routes_that_resolve(
|
||||||
left_behind in proptest::collection::vec((0u8..16, 0u8..3), 0..6),
|
left_behind in proptest::collection::vec((0u8..16, 0u8..3), 0..6),
|
||||||
steps in proptest::collection::vec(any_step(), 0..60),
|
steps in proptest::collection::vec(any_step(), 0..60)
|
||||||
) {
|
) {
|
||||||
let p = |n: u8| IpPrefix::new(IpVersion::Ipv4, [10, n, 0, 0], 16).unwrap();
|
let p = |n: u8| IpPrefix::new(IpVersion::Ipv4, [10, n, 0, 0], 16).unwrap();
|
||||||
let t = |n: u8| target(&format!("192.0.2.{}", n + 2), 2);
|
let t = |n: u8| target(&format!("192.0.2.{}", n + 2), 2);
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use bgp_fib::{Fib, FibConfig, FibError};
|
use bgp_fib::{Fib, FibConfig, FibError};
|
||||||
use bgp_packet::message::{
|
use bgp_packet::message::{
|
||||||
AsPathAttribute, MultiExitDiscPathAttribute, NextHopPathAttribute, NlriNextHop,
|
AsPathAttribute, CommunitiesPathAttribute, MultiExitDiscPathAttribute, NextHopPathAttribute,
|
||||||
OriginPathAttribute, PathAttribute,
|
NlriNextHop, OriginPathAttribute, PathAttribute,
|
||||||
};
|
};
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
use bgp_primitives::ip_prefix::IpPrefix;
|
||||||
use bgp_rib::table::{AttrSet, PeerIndex, PeerInfo, RibTable};
|
use bgp_rib::table::{AttrSet, PeerIndex, PeerInfo, RibTable};
|
||||||
@@ -499,6 +499,65 @@ async fn a_far_next_hop_goes_the_way_the_kernels_routes_say() {
|
|||||||
machine.kernel_becomes(&[]);
|
machine.kernel_becomes(&[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A route that asks for its packets to be dropped goes into the kernel
|
||||||
|
/// as a blackhole route, whatever its next hop, and comes out again with
|
||||||
|
/// the request.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||||
|
async fn a_blackhole_request_becomes_a_blackhole_route() {
|
||||||
|
let machine = Machine::new(13);
|
||||||
|
let peer = machine.peer(2);
|
||||||
|
// Asked for from a next hop nothing is a neighbor of, and with one
|
||||||
|
// ordinary route beside it to show the two apart.
|
||||||
|
let mut attributes = vec![
|
||||||
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||||
|
AsPathAttribute::from_asns(vec![65002]),
|
||||||
|
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(172, 31, 11, 9))),
|
||||||
|
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65002, 1), (0xFFFF, 0x029A)])),
|
||||||
|
];
|
||||||
|
let blackhole = machine.rib.attrs().intern(&attributes, None).unwrap();
|
||||||
|
attributes.pop();
|
||||||
|
attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
||||||
|
(65002, 1),
|
||||||
|
])));
|
||||||
|
let plain = machine.rib.attrs().intern(&attributes, None).unwrap();
|
||||||
|
machine.rib.announce(
|
||||||
|
peer,
|
||||||
|
&blackhole,
|
||||||
|
&[prefix("203.0.113.9/32"), prefix("2001:db8:100::9/128")],
|
||||||
|
);
|
||||||
|
machine
|
||||||
|
.rib
|
||||||
|
.announce(peer, &plain, &[prefix("198.18.0.0/15")]);
|
||||||
|
let (fib, task) = machine.start();
|
||||||
|
// The kernel puts an IPv6 blackhole route on lo, and says so.
|
||||||
|
machine.kernel_becomes(&[
|
||||||
|
"blackhole 203.0.113.9 proto bgp metric 20".into(),
|
||||||
|
"blackhole 2001:db8:100::9 dev lo proto bgp metric 20".into(),
|
||||||
|
]);
|
||||||
|
status_becomes(&fib, "two blackholes, one unresolved", |status| {
|
||||||
|
status.synced
|
||||||
|
&& status.installed == 2
|
||||||
|
&& status.blackholes == 2
|
||||||
|
&& status.unresolved == [("no route to the next hop", 1)]
|
||||||
|
});
|
||||||
|
|
||||||
|
// The request is withdrawn: the blackhole goes.
|
||||||
|
machine.rib.withdraw(peer, &[prefix("203.0.113.9/32")]);
|
||||||
|
machine.kernel_becomes(&["blackhole 2001:db8:100::9 dev lo proto bgp metric 20".into()]);
|
||||||
|
|
||||||
|
// A blackhole left by an earlier run is found and kept like any other.
|
||||||
|
fib.leave().await;
|
||||||
|
task.await.unwrap().unwrap();
|
||||||
|
let (fib, task) = machine.start();
|
||||||
|
status_becomes(&fib, "the blackhole is adopted", |status| {
|
||||||
|
status.synced && status.installed == 1 && status.blackholes == 1
|
||||||
|
});
|
||||||
|
fib.stop().await;
|
||||||
|
task.await.unwrap().unwrap();
|
||||||
|
machine.kernel_becomes(&[]);
|
||||||
|
}
|
||||||
|
|
||||||
/// A table's worth of routes goes in, and comes out, in reasonable time.
|
/// A table's worth of routes goes in, and comes out, in reasonable time.
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub const TIMEOUT: Duration = Duration::from_secs(5);
|
|||||||
pub fn default_policy() -> bgp_server::policy::ImportPolicy {
|
pub fn default_policy() -> bgp_server::policy::ImportPolicy {
|
||||||
bgp_server::policy::ImportPolicy {
|
bgp_server::policy::ImportPolicy {
|
||||||
accept_third_party_next_hop: true,
|
accept_third_party_next_hop: true,
|
||||||
|
accept_blackholes: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
//! Blackhole requests (RFC 7999): refused by default, and with
|
||||||
|
//! `accept_blackholes` taken for one address inside what the peer may
|
||||||
|
//! announce, whatever their next hop.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bgp_integration_tests::announcement::announcement;
|
||||||
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
|
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||||
|
use bgp_integration_tests::{default_policy, prefix, wait_until};
|
||||||
|
use bgp_server::policy::{ImportPolicy, PrefixSet};
|
||||||
|
use eyre::Result;
|
||||||
|
|
||||||
|
const LOCAL_ASN: u32 = 1299;
|
||||||
|
const PEER_ASN: u32 = 3333;
|
||||||
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||||
|
const BLACKHOLE: (u16, u16) = (0xFFFF, 0x029A);
|
||||||
|
|
||||||
|
async fn server_with(import: ImportPolicy) -> Result<ServerWithPeer> {
|
||||||
|
TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||||
|
c.connect_retry = Duration::from_millis(50);
|
||||||
|
peer.import = import;
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until the server has taken `taken` routes of the session's and
|
||||||
|
/// refused `refused`, and returns the prefixes in its table.
|
||||||
|
async fn table_after(server: &TestServer, taken: u64, refused: u64) -> Result<BTreeSet<String>> {
|
||||||
|
let counters = server.registry.list()[0].counters.clone();
|
||||||
|
wait_until("the routes are dealt with", || {
|
||||||
|
(counters.routes(), counters.rejected()) == (taken, refused)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
eyre::eyre!(
|
||||||
|
"the server took {} routes and refused {}; expected {taken} and {refused}",
|
||||||
|
counters.routes(),
|
||||||
|
counters.rejected()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(server
|
||||||
|
.received_routes()
|
||||||
|
.iter()
|
||||||
|
.map(|route| route.prefix.to_string())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The policy that lets the peer announce 193.0.0.0/21 and nothing
|
||||||
|
/// longer than a /24 inside it, as a customer's would.
|
||||||
|
fn customer_policy(accept_blackholes: bool) -> ImportPolicy {
|
||||||
|
let mut prefixes = PrefixSet::default();
|
||||||
|
prefixes.allow(prefix("193.0.0.0/21"), 24);
|
||||||
|
ImportPolicy {
|
||||||
|
prefixes: Some(prefixes),
|
||||||
|
accept_blackholes,
|
||||||
|
..default_policy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_blackhole_requests_are_refused_unless_accepted() -> Result<()> {
|
||||||
|
let with_peer = server_with(customer_policy(false)).await?;
|
||||||
|
let server = &with_peer.server;
|
||||||
|
let mut peer: TestPeer = server.established(PEER_ASN, PEER_ID).await?;
|
||||||
|
peer.announce(announcement(&["193.0.0.9/32"], &[PEER_ASN]).with_communities(&[BLACKHOLE]))
|
||||||
|
.await?;
|
||||||
|
peer.announce(announcement(&["193.0.0.0/24"], &[PEER_ASN]))
|
||||||
|
.await?;
|
||||||
|
assert_eq!(table_after(server, 1, 1).await?, set(&["193.0.0.0/24"]));
|
||||||
|
let refused = server.rejections.peers();
|
||||||
|
assert_eq!(refused.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
refused[0]
|
||||||
|
.1
|
||||||
|
.by_reason()
|
||||||
|
.iter()
|
||||||
|
.map(|(reason, n)| (reason.as_str(), *n))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[("blackhole request", 1)]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_a_blackhole_request_is_taken_for_one_address_in_the_peers_space() -> Result<()> {
|
||||||
|
let with_peer = server_with(customer_policy(true)).await?;
|
||||||
|
let server = &with_peer.server;
|
||||||
|
let mut peer: TestPeer = server.established(PEER_ASN, PEER_ID).await?;
|
||||||
|
// One address inside the peer's prefix, from a next hop that is no
|
||||||
|
// neighbor and would not pass as an ordinary route's.
|
||||||
|
peer.announce(
|
||||||
|
announcement(&["193.0.0.9/32"], &[PEER_ASN])
|
||||||
|
.with_next_hop(Ipv4Addr::new(172, 31, 0, 9))
|
||||||
|
.with_communities(&[BLACKHOLE]),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
// More than one address: not a blackhole request that is taken.
|
||||||
|
peer.announce(announcement(&["193.0.0.0/25"], &[PEER_ASN]).with_communities(&[BLACKHOLE]))
|
||||||
|
.await?;
|
||||||
|
// One address outside the peer's space.
|
||||||
|
peer.announce(announcement(&["193.0.8.9/32"], &[PEER_ASN]).with_communities(&[BLACKHOLE]))
|
||||||
|
.await?;
|
||||||
|
// And an ordinary route, taken as ever.
|
||||||
|
peer.announce(announcement(&["193.0.0.0/24"], &[PEER_ASN]))
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
table_after(server, 2, 2).await?,
|
||||||
|
set(&["193.0.0.0/24", "193.0.0.9/32"])
|
||||||
|
);
|
||||||
|
let refused = server.rejections.peers();
|
||||||
|
let mut reasons: Vec<(&str, u64)> = refused[0]
|
||||||
|
.1
|
||||||
|
.by_reason()
|
||||||
|
.iter()
|
||||||
|
.map(|(reason, n)| (reason.as_str(), *n))
|
||||||
|
.collect();
|
||||||
|
reasons.sort();
|
||||||
|
assert_eq!(
|
||||||
|
reasons,
|
||||||
|
[
|
||||||
|
("blackhole for more than one address", 1),
|
||||||
|
("prefix not allowed", 1)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// The route is in the table with its request on it, for the kernel
|
||||||
|
// and for whoever it goes on to.
|
||||||
|
let routes = server.received_routes();
|
||||||
|
let blackhole = routes
|
||||||
|
.iter()
|
||||||
|
.find(|route| route.prefix == prefix("193.0.0.9/32"))
|
||||||
|
.expect("the blackhole route is in the table");
|
||||||
|
assert!(ImportPolicy::is_blackhole_request(&blackhole.attributes));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(prefixes: &[&str]) -> BTreeSet<String> {
|
||||||
|
prefixes.iter().map(|p| p.to_string()).collect()
|
||||||
|
}
|
||||||
@@ -209,6 +209,23 @@ impl AttrSet {
|
|||||||
plain
|
plain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the route carries the BLACKHOLE community (RFC 7999), which
|
||||||
|
/// asks for the packets to its prefix to be dropped. Read off the
|
||||||
|
/// stored bytes without building anything else, for those who ask it
|
||||||
|
/// of every route.
|
||||||
|
pub fn is_blackhole(&self) -> bool {
|
||||||
|
const BLACKHOLE: [u8; 4] = [0xFF, 0xFF, 0x02, 0x9A];
|
||||||
|
let mut wire = &*self.wire;
|
||||||
|
while let Ok((attribute, rest)) = split_attribute(wire) {
|
||||||
|
if attribute.type_code == attr_type::COMMUNITIES {
|
||||||
|
let (communities, _) = attribute.value.as_chunks::<4>();
|
||||||
|
return communities.contains(&BLACKHOLE);
|
||||||
|
}
|
||||||
|
wire = rest;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Puts the AS numbers of the route's path in `out`, in place of what
|
/// Puts the AS numbers of the route's path in `out`, in place of what
|
||||||
/// was there: in the order of the path, the neighbor's first and the
|
/// was there: in the order of the path, the neighbor's first and the
|
||||||
/// origin's last, sets and all. Returns whether the path has a set in
|
/// origin's last, sets and all. Returns whether the path has a set in
|
||||||
@@ -595,6 +612,28 @@ mod tests {
|
|||||||
assert!(path.is_empty());
|
assert!(path.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The BLACKHOLE community is read off the stored bytes.
|
||||||
|
#[test]
|
||||||
|
fn a_blackhole_request_is_told_from_the_bytes() {
|
||||||
|
let store = AttrStore::new();
|
||||||
|
let with = |communities: Vec<(u16, u16)>| {
|
||||||
|
let mut attributes = attributes(vec![3333]);
|
||||||
|
attributes.retain(|attribute| !matches!(attribute, PathAttribute::Communities(_)));
|
||||||
|
attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
|
||||||
|
communities,
|
||||||
|
)));
|
||||||
|
store.intern(&attributes, None).unwrap()
|
||||||
|
};
|
||||||
|
assert!(with(vec![(3333, 100), (0xFFFF, 0x029A)]).is_blackhole());
|
||||||
|
assert!(!with(vec![(3333, 666), (0xFFFF, 0xFF01)]).is_blackhole());
|
||||||
|
assert!(
|
||||||
|
!store
|
||||||
|
.intern(&attributes(vec![3333]), None)
|
||||||
|
.unwrap()
|
||||||
|
.is_blackhole()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Two of the same attribute are not a set: its readers could each
|
/// Two of the same attribute are not a set: its readers could each
|
||||||
/// take a different one.
|
/// take a different one.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -572,6 +572,18 @@ message ImportPolicy {
|
|||||||
// looked at. A rule sees the route as the statements above leave it:
|
// looked at. A rule sees the route as the statements above leave it:
|
||||||
// with our communities taken off and ours put on.
|
// with our communities taken off and ours put on.
|
||||||
repeated ImportRule rules = 20;
|
repeated ImportRule rules = 20;
|
||||||
|
|
||||||
|
// By default a route with the BLACKHOLE community (65535:666, RFC
|
||||||
|
// 7999), which asks for the traffic to its prefix to be dropped, is
|
||||||
|
// refused ("blackhole request"). True takes such a route if it is for
|
||||||
|
// one address (a /32 or /128), whatever the longest prefix taken
|
||||||
|
// otherwise, and inside what the peer may announce; its next hop is
|
||||||
|
// not looked at. The traffic to the address is then dropped by this
|
||||||
|
// router (a blackhole route in the kernel), and the route goes on to
|
||||||
|
// the peers the export policy sends it to, community and all, for them
|
||||||
|
// to do the same. For a customer under attack, and for a route server
|
||||||
|
// whose blackhole routes are to be honoured.
|
||||||
|
bool accept_blackholes = 21;
|
||||||
}
|
}
|
||||||
|
|
||||||
// What is done to the routes with some communities, or for some prefixes:
|
// What is done to the routes with some communities, or for some prefixes:
|
||||||
|
|||||||
@@ -642,6 +642,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_blackholes: import.accept_blackholes,
|
||||||
own_prefixes: Arc::default(),
|
own_prefixes: Arc::default(),
|
||||||
originated: Arc::default(),
|
originated: Arc::default(),
|
||||||
accept_control_communities: import.accept_control_communities,
|
accept_control_communities: import.accept_control_communities,
|
||||||
|
|||||||
+110
-9
@@ -91,6 +91,9 @@ 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 routes with the BLACKHOLE community for single addresses, and
|
||||||
|
/// have the kernel drop the traffic to them (RFC 7999).
|
||||||
|
pub accept_blackholes: bool,
|
||||||
/// The prefixes this router originates. Those, and what is inside
|
/// The prefixes this router originates. Those, and what is inside
|
||||||
/// them, are ours and not taken from another network, unless
|
/// them, are ours and not taken from another network, unless
|
||||||
/// `prefixes` says in so many words that this peer may announce them:
|
/// `prefixes` says in so many words that this peer may announce them:
|
||||||
@@ -196,6 +199,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_blackholes: false,
|
||||||
own_prefixes: Arc::default(),
|
own_prefixes: Arc::default(),
|
||||||
originated: Arc::default(),
|
originated: Arc::default(),
|
||||||
accept_control_communities: false,
|
accept_control_communities: false,
|
||||||
@@ -224,6 +228,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_blackholes: false,
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,10 +341,14 @@ pub enum Rejection {
|
|||||||
/// The path has an AS_SET in it (RFC 9774).
|
/// The path has an AS_SET in it (RFC 9774).
|
||||||
AsSet,
|
AsSet,
|
||||||
/// The route asks for the traffic to its prefix to be dropped: it has
|
/// The route asks for the traffic to its prefix to be dropped: it has
|
||||||
/// the BLACKHOLE community (RFC 7999). Such requests are not acted on,
|
/// the BLACKHOLE community (RFC 7999), and the policy does not take
|
||||||
/// and the route is not taken as an ordinary one either, which would
|
/// such requests (`accept_blackholes`). The route is not taken as an
|
||||||
/// send the traffic to wherever its next hop drops it.
|
/// ordinary one either, which would send the traffic to wherever its
|
||||||
|
/// next hop drops it.
|
||||||
Blackhole,
|
Blackhole,
|
||||||
|
/// A blackhole request for more than one address: only a /32 or a
|
||||||
|
/// /128 is taken as one.
|
||||||
|
BlackholeNotHost,
|
||||||
/// The next hop is no address packets could be sent to, or is our
|
/// The next hop is no address packets could be sent to, or is our
|
||||||
/// own, or is an IPv6 address for an IPv4 route on a session that did
|
/// own, or is an IPv6 address for an IPv4 route on a session that did
|
||||||
/// not agree to that (RFC 8950).
|
/// not agree to that (RFC 8950).
|
||||||
@@ -371,7 +380,7 @@ impl Rejection {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ALL: [Self; 20] = [
|
pub const ALL: [Self; 21] = [
|
||||||
Self::BogonPrefix,
|
Self::BogonPrefix,
|
||||||
Self::TooShort,
|
Self::TooShort,
|
||||||
Self::TooSpecific,
|
Self::TooSpecific,
|
||||||
@@ -386,6 +395,7 @@ impl Rejection {
|
|||||||
Self::Leak,
|
Self::Leak,
|
||||||
Self::AsSet,
|
Self::AsSet,
|
||||||
Self::Blackhole,
|
Self::Blackhole,
|
||||||
|
Self::BlackholeNotHost,
|
||||||
Self::NextHopUnusable,
|
Self::NextHopUnusable,
|
||||||
Self::NextHopNotPeer,
|
Self::NextHopNotPeer,
|
||||||
Self::OwnPrefix,
|
Self::OwnPrefix,
|
||||||
@@ -415,6 +425,7 @@ impl Rejection {
|
|||||||
Self::Leak => "route leak (Only to Customer)",
|
Self::Leak => "route leak (Only to Customer)",
|
||||||
Self::AsSet => "AS_SET in path",
|
Self::AsSet => "AS_SET in path",
|
||||||
Self::Blackhole => "blackhole request",
|
Self::Blackhole => "blackhole request",
|
||||||
|
Self::BlackholeNotHost => "blackhole for more than one address",
|
||||||
Self::NextHopUnusable => "next hop unusable",
|
Self::NextHopUnusable => "next hop unusable",
|
||||||
Self::NextHopNotPeer => "next hop is not the peer",
|
Self::NextHopNotPeer => "next hop is not the peer",
|
||||||
Self::OwnPrefix => "prefix is our own",
|
Self::OwnPrefix => "prefix is our own",
|
||||||
@@ -568,11 +579,7 @@ impl ImportPolicy {
|
|||||||
if facts.has_local_asn {
|
if facts.has_local_asn {
|
||||||
return Err(Rejection::OwnAs);
|
return Err(Rejection::OwnAs);
|
||||||
}
|
}
|
||||||
let blackhole = attributes.iter().any(|attribute| {
|
if !self.accept_blackholes && Self::is_blackhole_request(attributes) {
|
||||||
matches!(attribute, PathAttribute::Communities(communities)
|
|
||||||
if communities.0.contains(&BLACKHOLE))
|
|
||||||
});
|
|
||||||
if blackhole {
|
|
||||||
return Err(Rejection::Blackhole);
|
return Err(Rejection::Blackhole);
|
||||||
}
|
}
|
||||||
// RFC 9234 §5, ingress, 1 and 2. A route that is only to go to
|
// RFC 9234 §5, ingress, 1 and 2. A route that is only to go to
|
||||||
@@ -651,6 +658,35 @@ impl ImportPolicy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the routes with these attributes ask for the traffic to
|
||||||
|
/// them to be dropped: the BLACKHOLE community (RFC 7999).
|
||||||
|
pub fn is_blackhole_request(attributes: &[PathAttribute]) -> bool {
|
||||||
|
attributes.iter().any(|attribute| {
|
||||||
|
matches!(attribute, PathAttribute::Communities(communities)
|
||||||
|
if communities.0.contains(&BLACKHOLE))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Judges the prefix of a blackhole request that the policy takes
|
||||||
|
/// (`accept_blackholes`): one address, and inside what the peer may
|
||||||
|
/// announce. The longest prefix taken otherwise does not apply, nor
|
||||||
|
/// does the longest allowed inside a listed prefix: a request is for
|
||||||
|
/// one address by its nature.
|
||||||
|
pub fn check_blackhole_prefix(&self, prefix: &IpPrefix) -> Result<(), Rejection> {
|
||||||
|
if prefix.length() != prefix.address_family().bits() {
|
||||||
|
return Err(Rejection::BlackholeNotHost);
|
||||||
|
}
|
||||||
|
if !self.accept_bogons && is_bogon_prefix(prefix) {
|
||||||
|
return Err(Rejection::BogonPrefix);
|
||||||
|
}
|
||||||
|
match &self.prefixes {
|
||||||
|
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`.
|
||||||
///
|
///
|
||||||
/// It has to be an address packets can be sent to and not our own
|
/// It has to be an address packets can be sent to and not our own
|
||||||
@@ -882,6 +918,15 @@ impl PrefixSet {
|
|||||||
self.entries.is_empty()
|
self.entries.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether `prefix` is inside some entry, whatever length the entry
|
||||||
|
/// allows inside it.
|
||||||
|
pub fn covers(&self, prefix: &IpPrefix) -> bool {
|
||||||
|
self.lengths[Self::version_index(prefix)]
|
||||||
|
.iter()
|
||||||
|
.take_while(|length| **length <= prefix.length())
|
||||||
|
.any(|length| self.entries.contains_key(&prefix.shortened(*length)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether `prefix` is allowed by some entry.
|
/// Whether `prefix` is allowed by some entry.
|
||||||
pub fn allows(&self, prefix: &IpPrefix) -> bool {
|
pub fn allows(&self, prefix: &IpPrefix) -> bool {
|
||||||
// An entry that covers it is the prefix cut to the entry's length,
|
// An entry that covers it is the prefix cut to the entry's length,
|
||||||
@@ -1362,6 +1407,62 @@ mod tests {
|
|||||||
assert_eq!(lenient.check_path(&with_set, &EXTERNAL), Ok(None));
|
assert_eq!(lenient.check_path(&with_set, &EXTERNAL), Ok(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// With accept_blackholes, a request to drop traffic is taken for one
|
||||||
|
/// address inside what the peer may announce, however long a prefix
|
||||||
|
/// the policy takes otherwise; and for nothing else.
|
||||||
|
#[test]
|
||||||
|
fn a_blackhole_request_is_taken_for_one_address_where_allowed() {
|
||||||
|
let mut attributes = path(&[3333, 2121]);
|
||||||
|
attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
||||||
|
(0xFFFF, 0x029A),
|
||||||
|
])));
|
||||||
|
assert!(ImportPolicy::is_blackhole_request(&attributes));
|
||||||
|
assert!(!ImportPolicy::is_blackhole_request(&path(&[3333])));
|
||||||
|
let mut policy = ImportPolicy {
|
||||||
|
accept_blackholes: true,
|
||||||
|
..ImportPolicy::default()
|
||||||
|
};
|
||||||
|
// The path passes as any other's would.
|
||||||
|
assert_eq!(policy.check_path(&attributes, &EXTERNAL), Ok(Some(2121)));
|
||||||
|
|
||||||
|
// Without a prefix list: any one routable address but ours.
|
||||||
|
let check = |policy: &ImportPolicy, p: &str| policy.check_blackhole_prefix(&prefix(p));
|
||||||
|
assert_eq!(check(&policy, "193.0.0.9/32"), Ok(()));
|
||||||
|
assert_eq!(check(&policy, "2001:67c:2e8::9/128"), Ok(()));
|
||||||
|
assert_eq!(
|
||||||
|
check(&policy, "193.0.0.0/24"),
|
||||||
|
Err(Rejection::BlackholeNotHost)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
check(&policy, "2001:67c:2e8::/64"),
|
||||||
|
Err(Rejection::BlackholeNotHost)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
check(&policy, "10.0.0.1/32"),
|
||||||
|
Err(Rejection::BogonPrefix),
|
||||||
|
"a bogon is a bogon, one address or not"
|
||||||
|
);
|
||||||
|
let mut ours = PrefixSet::default();
|
||||||
|
ours.allow(prefix("193.0.0.0/21"), 24);
|
||||||
|
policy.own_prefixes = Arc::new(ours);
|
||||||
|
assert_eq!(check(&policy, "193.0.0.9/32"), Err(Rejection::OwnPrefix));
|
||||||
|
|
||||||
|
// With one: inside a listed prefix, whatever length the list
|
||||||
|
// allows inside it, and ours or not.
|
||||||
|
let mut allowed = PrefixSet::default();
|
||||||
|
allowed.allow(prefix("193.0.0.0/21"), 24);
|
||||||
|
policy.prefixes = Some(allowed);
|
||||||
|
assert_eq!(check(&policy, "193.0.0.9/32"), Ok(()));
|
||||||
|
assert_eq!(
|
||||||
|
check(&policy, "193.0.8.9/32"),
|
||||||
|
Err(Rejection::PrefixNotAllowed)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Rejection::BlackholeNotHost.as_str(),
|
||||||
|
"blackhole for more than one address"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// A request to drop traffic is neither acted on nor taken as an
|
/// A request to drop traffic is neither acted on nor taken as an
|
||||||
/// ordinary route, whose next hop would drop the traffic all the same.
|
/// ordinary route, whose next hop would drop the traffic all the same.
|
||||||
/// It is told apart from a prefix that is merely too long, which is
|
/// It is told apart from a prefix that is merely too long, which is
|
||||||
|
|||||||
@@ -49,9 +49,22 @@ pub(crate) struct NextHops<'a> {
|
|||||||
|
|
||||||
impl NextHops<'_> {
|
impl NextHops<'_> {
|
||||||
/// Whether the kernel can be given a route for `prefix` by way of
|
/// Whether the kernel can be given a route for `prefix` by way of
|
||||||
/// `next_hop`.
|
/// `next_hop`; or, for a route that asks for its packets to be
|
||||||
pub(crate) fn reaches(&self, prefix: &IpPrefix, next_hop: Option<&NlriNextHop>) -> bool {
|
/// dropped (`blackhole`), as a route to nowhere.
|
||||||
resolve(self.topology, prefix, next_hop, Some(self.neighbor)).is_ok()
|
pub(crate) fn reaches(
|
||||||
|
&self,
|
||||||
|
prefix: &IpPrefix,
|
||||||
|
next_hop: Option<&NlriNextHop>,
|
||||||
|
blackhole: bool,
|
||||||
|
) -> bool {
|
||||||
|
resolve(
|
||||||
|
self.topology,
|
||||||
|
prefix,
|
||||||
|
next_hop,
|
||||||
|
Some(self.neighbor),
|
||||||
|
blackhole,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The two halves of [`NextHops::reaches`], for an UPDATE's worth of
|
/// The two halves of [`NextHops::reaches`], for an UPDATE's worth of
|
||||||
@@ -180,10 +193,22 @@ fn now_unreachable(
|
|||||||
if topology.interfaces.has_network_around(&route.prefix) {
|
if topology.interfaces.has_network_around(&route.prefix) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// A blackhole resolves whatever its next hop: nothing of it can
|
||||||
|
// have become unreachable.
|
||||||
|
if route.attrs.is_blackhole() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let next_hop = route.attrs.next_hop();
|
let next_hop = route.attrs.next_hop();
|
||||||
let key = (next_hop, route.prefix.address_family(), route.peer);
|
let key = (next_hop, route.prefix.address_family(), route.peer);
|
||||||
!*resolved.borrow_mut().entry(key).or_insert_with(|| {
|
!*resolved.borrow_mut().entry(key).or_insert_with(|| {
|
||||||
resolve(topology, &route.prefix, next_hop.as_ref(), Some(*neighbor)).is_ok()
|
resolve(
|
||||||
|
topology,
|
||||||
|
&route.prefix,
|
||||||
|
next_hop.as_ref(),
|
||||||
|
Some(*neighbor),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let mut by_peer: HashMap<PeerIndex, Vec<Nlri>> = HashMap::new();
|
let mut by_peer: HashMap<PeerIndex, Vec<Nlri>> = HashMap::new();
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ pub(crate) fn apply(
|
|||||||
as_path: OnceCell::new(),
|
as_path: OnceCell::new(),
|
||||||
refused_attributes: OnceCell::new(),
|
refused_attributes: OnceCell::new(),
|
||||||
mark: only_to_customer_mark(import, update),
|
mark: only_to_customer_mark(import, update),
|
||||||
|
blackhole: import.policy.accept_blackholes
|
||||||
|
&& ImportPolicy::is_blackhole_request(&update.path_attributes),
|
||||||
changed: OnceCell::new(),
|
changed: OnceCell::new(),
|
||||||
rules_holding: OnceCell::new(),
|
rules_holding: OnceCell::new(),
|
||||||
};
|
};
|
||||||
@@ -176,6 +178,10 @@ struct Applying<'a> {
|
|||||||
/// What the policy makes of the path, and the AS the routes originate
|
/// What the policy makes of the path, and the AS the routes originate
|
||||||
/// from if it passes.
|
/// from if it passes.
|
||||||
path_verdict: Result<Option<u32>, Rejection>,
|
path_verdict: Result<Option<u32>, Rejection>,
|
||||||
|
/// Whether the routes ask for their traffic to be dropped, and the
|
||||||
|
/// policy takes such requests: then the next hop is nothing to them,
|
||||||
|
/// and the prefix is judged as a request's.
|
||||||
|
blackhole: bool,
|
||||||
/// Whether the ASPAs find the path invalid: asked only if a route
|
/// Whether the ASPAs find the path invalid: asked only if a route
|
||||||
/// gets as far as that mattering.
|
/// gets as far as that mattering.
|
||||||
path_invalid: OnceCell<bool>,
|
path_invalid: OnceCell<bool>,
|
||||||
@@ -235,8 +241,10 @@ impl Applying<'_> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Asked once for all of them: they have the one next hop.
|
// Asked once for all of them: they have the one next hop. For a
|
||||||
let near = self
|
// blackhole it is not asked at all: the packets go nowhere.
|
||||||
|
let near = self.blackhole
|
||||||
|
|| self
|
||||||
.import
|
.import
|
||||||
.checks
|
.checks
|
||||||
.next_hops
|
.next_hops
|
||||||
@@ -374,9 +382,13 @@ impl Applying<'_> {
|
|||||||
.copied()
|
.copied()
|
||||||
.or_else(in_attributes)
|
.or_else(in_attributes)
|
||||||
.ok_or(Rejection::NextHopUnusable)?;
|
.ok_or(Rejection::NextHopUnusable)?;
|
||||||
|
// The traffic to a blackholed prefix goes nowhere: where the next
|
||||||
|
// hop would have taken it does not matter.
|
||||||
|
if !self.blackhole {
|
||||||
self.import
|
self.import
|
||||||
.policy
|
.policy
|
||||||
.check_next_hop(&next_hop, routes, &self.import.from)?;
|
.check_next_hop(&next_hop, routes, &self.import.from)?;
|
||||||
|
}
|
||||||
Ok((origin, next_hop))
|
Ok((origin, next_hop))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +397,11 @@ impl Applying<'_> {
|
|||||||
/// comes before what may pass.
|
/// comes before what may pass.
|
||||||
fn judge(&self, route: &Nlri, origin: Option<u32>, near: bool) -> Result<(), Rejection> {
|
fn judge(&self, route: &Nlri, origin: Option<u32>, near: bool) -> Result<(), Rejection> {
|
||||||
let import = self.import;
|
let import = self.import;
|
||||||
|
if self.blackhole {
|
||||||
|
import.policy.check_blackhole_prefix(&route.prefix)?;
|
||||||
|
} else {
|
||||||
import.policy.check_prefix(&route.prefix)?;
|
import.policy.check_prefix(&route.prefix)?;
|
||||||
|
}
|
||||||
let origin_invalid = import
|
let origin_invalid = import
|
||||||
.checks
|
.checks
|
||||||
.vrps
|
.vrps
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ impl<'a> Checks<'a> {
|
|||||||
if next_hops.is_own_network(prefix) {
|
if next_hops.is_own_network(prefix) {
|
||||||
return Some(Rejection::OwnNetwork);
|
return Some(Rejection::OwnNetwork);
|
||||||
}
|
}
|
||||||
if !next_hops.reaches(prefix, attrs.next_hop().as_ref()) {
|
if !next_hops.reaches(prefix, attrs.next_hop().as_ref(), attrs.is_blackhole()) {
|
||||||
return Some(Rejection::NextHopUnreachable);
|
return Some(Rejection::NextHopUnreachable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1111,6 +1111,9 @@ pub struct KernelView {
|
|||||||
/// How many of them are kept from before the daemon started though no
|
/// How many of them are kept from before the daemon started though no
|
||||||
/// peer has announced them again; they go when `stale_seconds` is up.
|
/// peer has announced them again; they go when `stale_seconds` is up.
|
||||||
pub kept: usize,
|
pub kept: usize,
|
||||||
|
/// How many of them are blackhole routes, which drop the packets for
|
||||||
|
/// their prefix (RFC 7999).
|
||||||
|
pub blackholes: usize,
|
||||||
/// Prefixes whose best route is not in the kernel because its next hop
|
/// Prefixes whose best route is not in the kernel because its next hop
|
||||||
/// cannot be used, by why.
|
/// cannot be used, by why.
|
||||||
pub unresolved: BTreeMap<&'static str, usize>,
|
pub unresolved: BTreeMap<&'static str, usize>,
|
||||||
@@ -1144,6 +1147,7 @@ pub async fn kernel(State(state): State<AppState>) -> Json<KernelView> {
|
|||||||
synced: status.synced,
|
synced: status.synced,
|
||||||
installed: status.installed,
|
installed: status.installed,
|
||||||
kept: status.kept,
|
kept: status.kept,
|
||||||
|
blackholes: status.blackholes,
|
||||||
unresolved: status.unresolved.into_iter().collect(),
|
unresolved: status.unresolved.into_iter().collect(),
|
||||||
refused: status.refused,
|
refused: status.refused,
|
||||||
errors: status.errors,
|
errors: status.errors,
|
||||||
|
|||||||
@@ -782,7 +782,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
const unresolved = Object.entries(k.unresolved).map(([why, n]) => `${fmtCount(n)} ${esc(why)}`).join(', ');
|
const unresolved = Object.entries(k.unresolved).map(([why, n]) => `${fmtCount(n)} ${esc(why)}`).join(', ');
|
||||||
const summary = '<table class="data kv">' +
|
const summary = '<table class="data kv">' +
|
||||||
row('state', `<span class="${k.synced ? 'green' : 'amber'}">${k.synced ? 'in step' : 'catching up'}</span> with the routing table`) +
|
row('state', `<span class="${k.synced ? 'green' : 'amber'}">${k.synced ? 'in step' : 'catching up'}</span> with the routing table`) +
|
||||||
row('routes installed', `<b>${fmtCount(k.installed)}</b>`) +
|
row('routes installed', `<b>${fmtCount(k.installed)}</b>${k.blackholes ? ` <span class="dim">of which ${fmtCount(k.blackholes)} ${k.blackholes === 1 ? 'is a blackhole' : 'are blackholes'}, dropping the packets for their prefix</span>` : ''}`) +
|
||||||
row('installed as', `<span class="mono">table ${k.table} metric ${k.metric} proto bgp</span>`) +
|
row('installed as', `<span class="mono">table ${k.table} metric ${k.metric} proto bgp</span>`) +
|
||||||
(k.kept ? row('kept from before the restart', `<span class="amber">${fmtCount(k.kept)}</span>, not announced again yet`) : '') +
|
(k.kept ? row('kept from before the restart', `<span class="amber">${fmtCount(k.kept)}</span>, not announced again yet`) : '') +
|
||||||
(unresolved ? row('not installed', `<span class="amber">${unresolved}</span>`) : '') +
|
(unresolved ? row('not installed', `<span class="amber">${unresolved}</span>`) : '') +
|
||||||
|
|||||||
@@ -62,23 +62,71 @@ disabled.
|
|||||||
| Route from an EBGP neighbor whose AS path contains the local AS number | `own AS in path` |
|
| Route from an EBGP neighbor whose AS path contains the local AS number | `own AS in path` |
|
||||||
| Route whose next hop is unusable: unspecified, loopback, multicast, or an address of the local router | `next hop unusable` |
|
| Route whose next hop is unusable: unspecified, loopback, multicast, or an address of the local router | `next hop unusable` |
|
||||||
| Route with the Only to Customer attribute received from the wrong direction (see [BGP Roles](@/docs/guides/roles.md)) | `route leak (Only to Customer)` |
|
| Route with the Only to Customer attribute received from the wrong direction (see [BGP Roles](@/docs/guides/roles.md)) | `route leak (Only to Customer)` |
|
||||||
| Route from an EBGP neighbor with the BLACKHOLE community (RFC 7999) | `blackhole request` |
|
| Route from an EBGP neighbor with the BLACKHOLE community (RFC 7999), unless `accept_blackholes` is set | `blackhole request` |
|
||||||
|
| Route with the BLACKHOLE community that is not a host route (/32 or /128), with `accept_blackholes` set | `blackhole for more than one address` |
|
||||||
|
|
||||||
### Blackhole Requests
|
### Blackhole Routes
|
||||||
|
|
||||||
A network under a denial-of-service attack can advertise a route for the
|
A network under a denial-of-service attack can advertise a route for the
|
||||||
attacked address with the BLACKHOLE community (65535:666), to request that
|
attacked address with the BLACKHOLE community (65535:666), to request that
|
||||||
other networks discard traffic to the address. At an Internet exchange, the
|
other networks discard traffic to the address (RFC 7999). At an Internet
|
||||||
route server distributes such routes to its members with the exchange's
|
exchange, the route server distributes such routes to its members with
|
||||||
blackhole address as the next hop. These routes are usually host routes
|
the exchange's blackhole address as the next hop. These routes are host
|
||||||
(/32 or /128).
|
routes (/32 or /128).
|
||||||
|
|
||||||
Ferrix does not support blackhole routing. It rejects every route with the
|
By default, Ferrix rejects every route with the BLACKHOLE community from
|
||||||
BLACKHOLE community from an EBGP neighbor, with the reason
|
an EBGP neighbor, with the reason `blackhole request`, regardless of the
|
||||||
`blackhole request`, regardless of the prefix length. A route with the
|
prefix length. Ferrix does not accept such a route as an ordinary route
|
||||||
BLACKHOLE community that is accepted as an ordinary route directs traffic
|
either, because that would direct traffic to the blackhole next hop,
|
||||||
to the blackhole next hop, which discards it, so Ferrix does not accept
|
which discards it.
|
||||||
these routes as ordinary routes either.
|
|
||||||
|
To honor blackhole requests from a neighbor, set `accept_blackholes` in
|
||||||
|
its import policy:
|
||||||
|
|
||||||
|
```textproto
|
||||||
|
peers {
|
||||||
|
name: "customer-a"
|
||||||
|
endpoint { address: "192.0.2.10" }
|
||||||
|
remote_asn: 64501
|
||||||
|
role: CUSTOMER
|
||||||
|
import {
|
||||||
|
prefixes { prefix: "203.0.113.0/24" }
|
||||||
|
accept_blackholes: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With `accept_blackholes` set, Ferrix accepts a route with the BLACKHOLE
|
||||||
|
community if all of the following conditions are met:
|
||||||
|
|
||||||
|
- The prefix is a single address: a /32 or a /128. The
|
||||||
|
`max_ipv4_prefix_length` and `max_ipv6_prefix_length` limits do not
|
||||||
|
apply to blackhole routes. A blackhole route for a longer prefix is
|
||||||
|
rejected with the reason `blackhole for more than one address`.
|
||||||
|
- The address is within a prefix that the neighbor may announce, if
|
||||||
|
`prefixes` is configured. The `max_length` of the entry does not apply.
|
||||||
|
- The route passes the AS path, origin, and RPKI checks that apply to
|
||||||
|
every route.
|
||||||
|
|
||||||
|
The next hop of a blackhole route is not checked, and need not be
|
||||||
|
reachable: Ferrix never forwards traffic to it. If the route is the active
|
||||||
|
route for its prefix, which a host route is unless a longer-matching
|
||||||
|
route exists, Ferrix installs a blackhole route in the kernel, and the
|
||||||
|
router discards traffic to the address. See [Blackhole
|
||||||
|
Routes](@/docs/guides/kernel.md#blackhole-routes) in the kernel guide.
|
||||||
|
|
||||||
|
Ferrix advertises an accepted blackhole route like any other route, with
|
||||||
|
the BLACKHOLE community, to the neighbors that the export policy and the
|
||||||
|
export limits permit. A provider that honors the community then discards
|
||||||
|
the traffic before it reaches the local network. To keep a blackhole
|
||||||
|
route from being advertised, the originator adds the NO_EXPORT or
|
||||||
|
NO_ADVERTISE community, as RFC 7999 recommends.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Ferrix does not install a blackhole route for a prefix that it
|
||||||
|
> originates with the BLACKHOLE community. To discard traffic locally for
|
||||||
|
> such a prefix, add a kernel blackhole route, for example
|
||||||
|
> `ip route add blackhole 203.0.113.9/32`.
|
||||||
|
|
||||||
### Next Hop Check
|
### Next Hop Check
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,18 @@ next hop becomes unresolvable is also moved to the held state.
|
|||||||
If the `kernel` statement is not configured, Ferrix has no information about
|
If the `kernel` statement is not configured, Ferrix has no information about
|
||||||
next hop reachability and accepts all routes.
|
next hop reachability and accepts all routes.
|
||||||
|
|
||||||
|
## Blackhole Routes
|
||||||
|
|
||||||
|
Ferrix installs the active route for a prefix as a kernel blackhole route
|
||||||
|
(`ip route` displays it as `blackhole PREFIX proto bgp`) if the route
|
||||||
|
carries the BLACKHOLE community (RFC 7999) and the import policy accepted
|
||||||
|
it with `accept_blackholes`. The kernel discards traffic to the prefix.
|
||||||
|
The next hop of the route is not resolved, and the route is installed
|
||||||
|
even if the next hop is unreachable. The kernel section of the status
|
||||||
|
page and `/api/kernel` report the number of installed blackhole routes.
|
||||||
|
See [Blackhole Routes](@/docs/guides/import-policy.md#blackhole-routes)
|
||||||
|
in the import policy guide.
|
||||||
|
|
||||||
## Connected Networks
|
## Connected Networks
|
||||||
|
|
||||||
Ferrix never installs a route for a subnet of a local interface, or for a
|
Ferrix never installs a route for a subnet of a local interface, or for a
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ accepted. See [Import Policy](@/docs/guides/import-policy.md).
|
|||||||
| `max_prefixes` <span class="type">uint32</span> | 0 (no limit) | Specifies the maximum number of routes that the neighbor can have in the routing table. If the limit is exceeded, Ferrix tears down the session and places the neighbor in hold-down. |
|
| `max_prefixes` <span class="type">uint32</span> | 0 (no limit) | Specifies the maximum number of routes that the neighbor can have in the routing table. If the limit is exceeded, Ferrix tears down the session and places the neighbor in hold-down. |
|
||||||
| `hold_down_seconds` <span class="type">uint32</span> | 600 | Specifies the hold-down time, in seconds, after the neighbor exceeds `max_prefixes`, or after the routes to be advertised to it exceed `export_limits.max_prefixes`. |
|
| `hold_down_seconds` <span class="type">uint32</span> | 600 | Specifies the hold-down time, in seconds, after the neighbor exceeds `max_prefixes`, or after the routes to be advertised to it exceed `export_limits.max_prefixes`. |
|
||||||
| `accept_bogons` <span class="type">bool</span> | `false` | Accepts bogon prefixes, and AS paths that contain private, reserved, or documentation AS numbers. |
|
| `accept_bogons` <span class="type">bool</span> | `false` | Accepts bogon prefixes, and AS paths that contain private, reserved, or documentation AS numbers. |
|
||||||
|
| `accept_blackholes` <span class="type">bool</span> | `false` | Accepts routes with the BLACKHOLE community (RFC 7999) for single addresses, and installs them as blackhole routes. See [Blackhole Routes](@/docs/guides/import-policy.md#blackhole-routes). |
|
||||||
| `accept_default_route` <span class="type">bool</span> | `false` | Accepts the default routes `0.0.0.0/0` and `::/0`. |
|
| `accept_default_route` <span class="type">bool</span> | `false` | Accepts the default routes `0.0.0.0/0` and `::/0`. |
|
||||||
| `max_ipv4_prefix_length` <span class="type">uint32</span> | 24 | Specifies the longest IPv4 prefix accepted. |
|
| `max_ipv4_prefix_length` <span class="type">uint32</span> | 24 | Specifies the longest IPv4 prefix accepted. |
|
||||||
| `max_ipv6_prefix_length` <span class="type">uint32</span> | 48 | Specifies the longest IPv6 prefix accepted. |
|
| `max_ipv6_prefix_length` <span class="type">uint32</span> | 48 | Specifies the longest IPv6 prefix accepted. |
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ routes. To return the records for one neighbor, add `?peer=NAME`.
|
|||||||
"synced": true,
|
"synced": true,
|
||||||
"installed": 0,
|
"installed": 0,
|
||||||
"kept": 0,
|
"kept": 0,
|
||||||
|
"blackholes": 0,
|
||||||
"unresolved": {},
|
"unresolved": {},
|
||||||
"refused": 0,
|
"refused": 0,
|
||||||
"errors": 0,
|
"errors": 0,
|
||||||
@@ -222,6 +223,7 @@ routes. To return the records for one neighbor, add `?peer=NAME`.
|
|||||||
| `synced` | Whether the kernel routing table has been synchronized with the routing table since the daemon started. |
|
| `synced` | Whether the kernel routing table has been synchronized with the routing table since the daemon started. |
|
||||||
| `installed` | Number of routes installed. |
|
| `installed` | Number of routes installed. |
|
||||||
| `kept` | Number of routes from a previous run that are retained during startup. |
|
| `kept` | Number of routes from a previous run that are retained during startup. |
|
||||||
|
| `blackholes` | Number of installed routes that are blackhole routes, which discard traffic for their prefix (RFC 7999). |
|
||||||
| `unresolved` | Number of prefixes whose active route is not installed, by reason. |
|
| `unresolved` | Number of prefixes whose active route is not installed, by reason. |
|
||||||
| `refused` | Number of prefixes whose active route the kernel rejected. |
|
| `refused` | Number of prefixes whose active route the kernel rejected. |
|
||||||
| `errors` | Number of requests that the kernel rejected. |
|
| `errors` | Number of requests that the kernel rejected. |
|
||||||
|
|||||||
@@ -203,8 +203,11 @@ The base protocol specification.
|
|||||||
| Feature | Status |
|
| Feature | Status |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| `BLACKHOLE` accepted as a community name in configuration | ✅ |
|
| `BLACKHOLE` accepted as a community name in configuration | ✅ |
|
||||||
| Routes with BLACKHOLE from EBGP neighbors rejected on import (`blackhole request`) | ✅ |
|
| Routes with BLACKHOLE from EBGP neighbors rejected on import by default (`blackhole request`) | ✅ |
|
||||||
| Blackhole routing: discarding traffic for prefixes with BLACKHOLE | ❌ |
|
| Blackhole routes for single addresses accepted with `accept_blackholes` (§3.2: prefix length limits relaxed, next hop not checked) | ✅ |
|
||||||
|
| Blackhole routing: a kernel blackhole route discards traffic for the prefix | ✅ |
|
||||||
|
| Blackhole routes advertised with the community, for providers to honor (§3.3) | ✅ |
|
||||||
|
| A blackhole route for a prefix this router originates | ❌ (add a kernel blackhole route by hand) |
|
||||||
|
|
||||||
## Other Capabilities (Wire Format Only)
|
## Other Capabilities (Wire Format Only)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user