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
+682
-96
No files matched your search
+45
-14
@@ -24,7 +24,8 @@ use tracing::{debug, info, warn};
|
||||
|
||||
use crate::interfaces::Interfaces;
|
||||
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::{
|
||||
HashMap as FastHashMap, Neighbor, Op, Plan, Target, Unresolved, resolve_next_hop,
|
||||
@@ -444,6 +445,8 @@ enum News {
|
||||
prefix: IpPrefix,
|
||||
peer: PeerIndex,
|
||||
next_hop: Option<NlriNextHop>,
|
||||
/// Whether the route asks for its packets to be dropped.
|
||||
blackhole: bool,
|
||||
},
|
||||
Withdrawn(IpPrefix),
|
||||
Synced,
|
||||
@@ -539,7 +542,7 @@ impl Task {
|
||||
&& let (Some(gateway), Some(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()
|
||||
}
|
||||
(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,
|
||||
};
|
||||
self.plan.adopt(route.prefix, target, keep);
|
||||
@@ -661,6 +665,9 @@ impl Task {
|
||||
if let Some(id) = known.get(&target) {
|
||||
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,
|
||||
// not for each route, and a route must not name one that is not
|
||||
// there yet.
|
||||
@@ -669,7 +676,7 @@ impl Task {
|
||||
.control
|
||||
.ask(
|
||||
|out, sequence| {
|
||||
netlink::nexthop_request(out, sequence, &target.gateway, target.interface);
|
||||
netlink::nexthop_request(out, sequence, &gateway, interface);
|
||||
},
|
||||
|message| {
|
||||
if let Message::Nexthop {
|
||||
@@ -684,7 +691,7 @@ impl Task {
|
||||
.await?;
|
||||
Ok(match (errno, made) {
|
||||
(0, Some(id)) => {
|
||||
debug!(id, gateway = %target.gateway, "Made a nexthop object");
|
||||
debug!(id, %gateway, "Made a nexthop object");
|
||||
self.owned.insert(id);
|
||||
if let Some(known) = &mut self.nexthops {
|
||||
known.insert(target, id);
|
||||
@@ -711,16 +718,29 @@ impl Task {
|
||||
kind: KIND_UNICAST,
|
||||
};
|
||||
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, .. } => {
|
||||
if self.nexthops.is_some() {
|
||||
match self.nexthop(*target).await? {
|
||||
Ok(id) => route.nexthop = Some(id),
|
||||
Err(errno) => return Ok(Err(errno)),
|
||||
}
|
||||
} else {
|
||||
route.gateway = Some(target.gateway);
|
||||
route.interface = Some(target.interface);
|
||||
} else if let Target::Via { gateway, interface } = target {
|
||||
route.gateway = Some(*gateway);
|
||||
route.interface = Some(*interface);
|
||||
}
|
||||
Change::Replace
|
||||
}
|
||||
@@ -789,14 +809,20 @@ impl Task {
|
||||
}
|
||||
let error = io::Error::from_raw_os_error(errno);
|
||||
let what = match op {
|
||||
Op::Install { prefix, target } => format!(
|
||||
"install {prefix} via {} on {}",
|
||||
target.gateway,
|
||||
self.topology.interfaces.link(target.interface).map_or_else(
|
||||
|| format!("interface {}", target.interface),
|
||||
Op::Install {
|
||||
prefix,
|
||||
target: Target::Via { gateway, interface },
|
||||
} => format!(
|
||||
"install {prefix} via {gateway} on {}",
|
||||
self.topology.interfaces.link(*interface).map_or_else(
|
||||
|| format!("interface {interface}"),
|
||||
|link| link.name.clone()
|
||||
)
|
||||
),
|
||||
Op::Install {
|
||||
prefix,
|
||||
target: Target::Discard,
|
||||
} => format!("install a blackhole for {prefix}"),
|
||||
Op::Remove { prefix } => format!("remove {prefix}"),
|
||||
};
|
||||
// Counted always, said out loud once in a while: a table's worth
|
||||
@@ -836,6 +862,7 @@ impl Task {
|
||||
prefix,
|
||||
peer,
|
||||
next_hop: attrs.next_hop(),
|
||||
blackhole: attrs.is_blackhole(),
|
||||
},
|
||||
Event::Withdrawn { prefix } => News::Withdrawn(prefix),
|
||||
// Not given to a follower of the best routes.
|
||||
@@ -858,6 +885,7 @@ impl Task {
|
||||
prefix,
|
||||
peer,
|
||||
next_hop,
|
||||
blackhole,
|
||||
} => {
|
||||
// Whose link a link-local next hop is on is asked only
|
||||
// where there is one.
|
||||
@@ -877,6 +905,8 @@ impl Task {
|
||||
// next hop: it is about the prefix.
|
||||
let target = if topology.interfaces.has_network_around(&prefix) {
|
||||
Err(Unresolved::OwnNetwork)
|
||||
} else if blackhole {
|
||||
Ok(Target::Discard)
|
||||
} else {
|
||||
*self
|
||||
.resolved
|
||||
@@ -1075,6 +1105,7 @@ impl Task {
|
||||
status.synced = self.synced;
|
||||
status.installed = self.plan.installed();
|
||||
status.kept = if self.keeping { self.plan.kept() } else { 0 };
|
||||
status.blackholes = self.plan.blackholes();
|
||||
status.refused = self.plan.refused();
|
||||
status.unresolved = self
|
||||
.plan
|
||||
|
||||
@@ -139,6 +139,9 @@ pub struct Status {
|
||||
/// How many of them are kept from an earlier run though the routing
|
||||
/// table does not have them (yet); see [`FibConfig::stale_time`].
|
||||
pub kept: usize,
|
||||
/// How many of them are blackhole routes, which drop the packets for
|
||||
/// their prefix.
|
||||
pub blackholes: usize,
|
||||
/// Prefixes whose best route is not in the kernel because its next
|
||||
/// hop is not usable, by why.
|
||||
pub unresolved: Vec<(&'static str, usize)>,
|
||||
|
||||
@@ -23,7 +23,11 @@ pub const TABLE_MAIN: u32 = 254;
|
||||
|
||||
/// A route that forwards to a gateway or out of an interface
|
||||
/// (`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;
|
||||
/// `RTN_BLACKHOLE`: a route that discards the packets for its prefix.
|
||||
pub const KIND_BLACKHOLE: u8 = 6;
|
||||
|
||||
const HEADER_LEN: usize = 16;
|
||||
|
||||
|
||||
+124
-41
@@ -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.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Target {
|
||||
pub gateway: IpAddr,
|
||||
/// The index of the interface the gateway is on.
|
||||
pub interface: u32,
|
||||
pub enum Target {
|
||||
/// To a gateway, out of an interface.
|
||||
Via {
|
||||
gateway: IpAddr,
|
||||
/// The index of the interface the gateway is on.
|
||||
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.
|
||||
@@ -105,7 +111,9 @@ impl From<IpAddr> for Neighbor {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// packets go. One that is not is looked up in the kernel's own routes,
|
||||
@@ -118,10 +126,14 @@ pub fn resolve(
|
||||
prefix: &IpPrefix,
|
||||
next_hop: Option<&NlriNextHop>,
|
||||
peer: Option<Neighbor>,
|
||||
blackhole: bool,
|
||||
) -> Result<Target, Unresolved> {
|
||||
if topology.interfaces.has_network_around(prefix) {
|
||||
return Err(Unresolved::OwnNetwork);
|
||||
}
|
||||
if blackhole {
|
||||
return Ok(Target::Discard);
|
||||
}
|
||||
resolve_next_hop(topology, prefix.address_family(), next_hop, peer)
|
||||
}
|
||||
|
||||
@@ -168,7 +180,7 @@ fn through_routes(
|
||||
if routes == IpVersion::Ipv6 && gateway.is_ipv4() {
|
||||
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
|
||||
@@ -198,7 +210,7 @@ pub fn resolve_next_hop(
|
||||
return Err(Unresolved::Ourselves);
|
||||
}
|
||||
match interfaces.connected(gateway) {
|
||||
Some(connected) => Ok(Target {
|
||||
Some(connected) => Ok(Target::Via {
|
||||
gateway,
|
||||
interface: connected.interface,
|
||||
}),
|
||||
@@ -226,7 +238,7 @@ pub fn resolve_next_hop(
|
||||
{
|
||||
return Err(Unresolved::Ourselves);
|
||||
}
|
||||
Ok(Target {
|
||||
Ok(Target::Via {
|
||||
gateway: gateway.into(),
|
||||
interface,
|
||||
})
|
||||
@@ -313,6 +325,14 @@ impl Plan {
|
||||
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
|
||||
/// kernel for want of a usable next hop.
|
||||
pub fn unresolved(&self) -> usize {
|
||||
@@ -507,7 +527,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn target(gateway: &str, interface: u32) -> Target {
|
||||
Target {
|
||||
Target::Via {
|
||||
gateway: gateway.parse().unwrap(),
|
||||
interface,
|
||||
}
|
||||
@@ -575,16 +595,16 @@ mod tests {
|
||||
let v6_hop = |s: &str| NlriNextHop::Ipv6(s.parse().unwrap());
|
||||
|
||||
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))
|
||||
);
|
||||
// A neighbor other than the peer: a route server's routes.
|
||||
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))
|
||||
);
|
||||
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))
|
||||
);
|
||||
for (hop, why) in [
|
||||
@@ -592,10 +612,13 @@ mod tests {
|
||||
(Some(v4_hop("192.0.2.1")), Unresolved::Ourselves),
|
||||
(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!(
|
||||
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)
|
||||
);
|
||||
// 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"),
|
||||
};
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &ours, Some(&hop), peer),
|
||||
resolve(&interfaces, &ours, Some(&hop), peer, false),
|
||||
Err(Unresolved::OwnNetwork),
|
||||
"{ours}"
|
||||
);
|
||||
@@ -622,14 +645,15 @@ mod tests {
|
||||
&interfaces,
|
||||
&prefix("192.0.0.0/22"),
|
||||
Some(&v4_hop("192.0.2.2")),
|
||||
peer
|
||||
peer,
|
||||
false
|
||||
),
|
||||
Ok(target("192.0.2.2", 2))
|
||||
);
|
||||
// An IPv4 route by way of an IPv6 neighbor: a link with only IPv6
|
||||
// addresses (RFC 8950).
|
||||
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))
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -641,6 +665,7 @@ mod tests {
|
||||
link_local: "fe80::2".parse().unwrap(),
|
||||
}),
|
||||
Some(neighbor("2001:db8::2")),
|
||||
false
|
||||
),
|
||||
Ok(target("fe80::2", 2))
|
||||
);
|
||||
@@ -658,26 +683,44 @@ mod tests {
|
||||
};
|
||||
// The global address is a neighbor: it is used.
|
||||
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))
|
||||
);
|
||||
// It is not: the link-local one, on the interface the peer is on.
|
||||
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))
|
||||
);
|
||||
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))
|
||||
);
|
||||
// A peer that is not a neighbor says nothing about the link.
|
||||
let far = Some(neighbor("2001:db8:ffff::9"));
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&NlriNextHop::Ipv6(link_local)), far),
|
||||
resolve(
|
||||
&interfaces,
|
||||
&v6,
|
||||
Some(&NlriNextHop::Ipv6(link_local)),
|
||||
far,
|
||||
false
|
||||
),
|
||||
Err(Unresolved::NoRoute)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&both("2001:db8::1")), peer),
|
||||
resolve(&interfaces, &v6, Some(&both("2001:db8::1")), peer, false),
|
||||
Err(Unresolved::Ourselves)
|
||||
);
|
||||
}
|
||||
@@ -722,7 +765,7 @@ mod tests {
|
||||
for interface in [2, 3] {
|
||||
for route in [&v4, &v6] {
|
||||
assert_eq!(
|
||||
resolve(&interfaces, route, Some(&next_hop), on(interface)),
|
||||
resolve(&interfaces, route, Some(&next_hop), on(interface), false),
|
||||
Ok(target("fe80::2", interface)),
|
||||
"{next_hop:?} on {interface}"
|
||||
);
|
||||
@@ -730,24 +773,24 @@ mod tests {
|
||||
}
|
||||
// An interface that is not there, or is down.
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&next_hop), on(9)),
|
||||
resolve(&interfaces, &v6, Some(&next_hop), on(9), false),
|
||||
Err(Unresolved::NoRoute)
|
||||
);
|
||||
}
|
||||
// Our own address on that link, which on another is a neighbor's.
|
||||
let ours = NlriNextHop::Ipv6("fe80::a".parse().unwrap());
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&ours), on(2)),
|
||||
resolve(&interfaces, &v6, Some(&ours), on(2), false),
|
||||
Err(Unresolved::Ourselves)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&ours), on(3)),
|
||||
resolve(&interfaces, &v6, Some(&ours), on(3), false),
|
||||
Ok(target("fe80::a", 3))
|
||||
);
|
||||
// A global next hop on such a session is looked up as any other.
|
||||
let global = NlriNextHop::Ipv6("2001:db8::2".parse().unwrap());
|
||||
assert_eq!(
|
||||
resolve(&interfaces, &v6, Some(&global), on(3)),
|
||||
resolve(&interfaces, &v6, Some(&global), on(3), false),
|
||||
Ok(target("2001:db8::2", 2))
|
||||
);
|
||||
}
|
||||
@@ -767,7 +810,7 @@ mod tests {
|
||||
|
||||
// Nothing says the way.
|
||||
assert_eq!(
|
||||
resolve(&topology, &v4, Some(&far), peer),
|
||||
resolve(&topology, &v4, Some(&far), peer, false),
|
||||
Err(Unresolved::NoRoute)
|
||||
);
|
||||
// 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)),
|
||||
);
|
||||
assert_eq!(
|
||||
resolve(&topology, &v4, Some(&far), peer),
|
||||
resolve(&topology, &v4, Some(&far), peer, false),
|
||||
Err(Unresolved::OnlyDefault)
|
||||
);
|
||||
topology.via_default = true;
|
||||
assert_eq!(
|
||||
resolve(&topology, &v4, Some(&far), peer),
|
||||
resolve(&topology, &v4, Some(&far), peer, false),
|
||||
Ok(target("192.0.2.254", 2))
|
||||
);
|
||||
topology.via_default = false;
|
||||
@@ -791,7 +834,7 @@ mod tests {
|
||||
kernel_route("10.9.0.0/16", Some("198.51.100.9"), Some(3)),
|
||||
);
|
||||
assert_eq!(
|
||||
resolve(&topology, &v4, Some(&far), peer),
|
||||
resolve(&topology, &v4, Some(&far), peer, false),
|
||||
Ok(target("198.51.100.9", 3))
|
||||
);
|
||||
// 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)));
|
||||
assert_eq!(
|
||||
resolve(&topology, &v4, Some(&far), peer),
|
||||
resolve(&topology, &v4, Some(&far), peer, false),
|
||||
Ok(target("10.9.9.9", 7))
|
||||
);
|
||||
// The tunnel goes down: the kernel keeps such routes, we do not
|
||||
@@ -820,7 +863,7 @@ mod tests {
|
||||
},
|
||||
});
|
||||
assert_eq!(
|
||||
resolve(&topology, &v4, Some(&far), peer),
|
||||
resolve(&topology, &v4, Some(&far), peer, false),
|
||||
Err(Unresolved::NoRoute)
|
||||
);
|
||||
// A route that names a nexthop object goes where the object says.
|
||||
@@ -832,7 +875,7 @@ mod tests {
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
resolve(&topology, &v4, Some(&far), peer),
|
||||
resolve(&topology, &v4, Some(&far), peer, false),
|
||||
Err(Unresolved::NoRoute),
|
||||
"an object the kernel has not told of"
|
||||
);
|
||||
@@ -846,7 +889,7 @@ mod tests {
|
||||
},
|
||||
});
|
||||
assert_eq!(
|
||||
resolve(&topology, &v4, Some(&far), peer),
|
||||
resolve(&topology, &v4, Some(&far), peer, false),
|
||||
Ok(target("192.0.2.77", 2))
|
||||
);
|
||||
// A route that discards packets is a way to nowhere.
|
||||
@@ -858,7 +901,7 @@ mod tests {
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
resolve(&topology, &v4, Some(&far), peer),
|
||||
resolve(&topology, &v4, Some(&far), peer, false),
|
||||
Err(Unresolved::Discarded)
|
||||
);
|
||||
// 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)),
|
||||
);
|
||||
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)
|
||||
);
|
||||
with_route(
|
||||
@@ -886,11 +929,11 @@ mod tests {
|
||||
kernel_route("2001:db8:9::/48", Some("2001:db8::9"), Some(2)),
|
||||
);
|
||||
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))
|
||||
);
|
||||
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))
|
||||
);
|
||||
// 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)),
|
||||
);
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn the_kernel_is_asked_only_for_what_changes() {
|
||||
let mut plan = Plan::default();
|
||||
@@ -1087,7 +1170,7 @@ mod tests {
|
||||
#[test]
|
||||
fn the_kernel_ends_up_with_the_best_routes_that_resolve(
|
||||
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 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_packet::message::{
|
||||
AsPathAttribute, MultiExitDiscPathAttribute, NextHopPathAttribute, NlriNextHop,
|
||||
OriginPathAttribute, PathAttribute,
|
||||
AsPathAttribute, CommunitiesPathAttribute, MultiExitDiscPathAttribute, NextHopPathAttribute,
|
||||
NlriNextHop, OriginPathAttribute, PathAttribute,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
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(&[]);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
|
||||
Reference in new issue
Block a user