From efb2d81a501d142d4e82047707a7bfbf726f9b8e Mon Sep 17 00:00:00 2001 From: Rayhaan Jaufeerally Date: Sun, 4 Oct 2026 01:03:16 +0200 Subject: [PATCH] Reject blackhole requests with a reason of their own A route with the BLACKHOLE community (RFC 7999) asks for the traffic to its prefix to be dropped. Exchange route servers pass these on as host routes with the exchange's blackhole address as next hop, and they were rejected as "prefix too specific", which hid what they are. Routes from EBGP neighbors with BLACKHOLE are now rejected as "blackhole request", whatever their length. One short enough to pass would have been taken as an ordinary route, sending the traffic to the blackhole next hop: the request honoured without anyone having chosen to. Acting on blackhole requests is not supported; the import policy page and the standards list say so. Co-Authored-By: Claude Opus 5.5 --- crates/server/src/policy.rs | 51 ++++++++++++++++++++++- docs/content/docs/guides/import-policy.md | 22 +++++++++- docs/content/docs/reference/rfcs.md | 8 ++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/crates/server/src/policy.rs b/crates/server/src/policy.rs index 399eb8a..718a529 100644 --- a/crates/server/src/policy.rs +++ b/crates/server/src/policy.rs @@ -218,6 +218,11 @@ pub enum Rejection { Leak, /// The path has an AS_SET in it (RFC 9774). AsSet, + /// 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, + /// and the route is not taken as an ordinary one either, which would + /// send the traffic to wherever its next hop drops it. + Blackhole, /// 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 /// not agree to that (RFC 8950). @@ -242,7 +247,7 @@ impl Rejection { ) } - pub const ALL: [Self; 17] = [ + pub const ALL: [Self; 18] = [ Self::BogonPrefix, Self::TooShort, Self::TooSpecific, @@ -256,6 +261,7 @@ impl Rejection { Self::AspaInvalid, Self::Leak, Self::AsSet, + Self::Blackhole, Self::NextHopUnusable, Self::NextHopNotPeer, Self::OwnPrefix, @@ -282,6 +288,7 @@ impl Rejection { Self::AspaInvalid => "ASPA invalid", Self::Leak => "route leak (Only to Customer)", Self::AsSet => "AS_SET in path", + Self::Blackhole => "blackhole request", Self::NextHopUnusable => "next hop unusable", Self::NextHopNotPeer => "next hop is not the peer", Self::OwnPrefix => "prefix is our own", @@ -290,6 +297,10 @@ impl Rejection { } } +/// The community that asks for the traffic to a prefix to be dropped +/// (RFC 7999). +const BLACKHOLE: (u16, u16) = (0xFFFF, 0x029A); + /// What a peer is to us, in the business between networks. It decides /// what its routes may look like: a customer's routes have come up to us /// through its own customers only, while a provider's may have been @@ -419,6 +430,13 @@ impl ImportPolicy { if facts.has_local_asn { return Err(Rejection::OwnAs); } + let blackhole = attributes.iter().any(|attribute| { + matches!(attribute, PathAttribute::Communities(communities) + if communities.0.contains(&BLACKHOLE)) + }); + if blackhole { + return Err(Rejection::Blackhole); + } // RFC 9234 §5, ingress, 1 and 2. A route that is only to go to // customers has been down or across once already. From a customer // it is coming up again; from a peer it is coming across again, @@ -852,7 +870,7 @@ impl HoldDowns { #[cfg(test)] mod tests { - use bgp_packet::message::AsPathSegment; + use bgp_packet::message::{AsPathSegment, CommunitiesPathAttribute}; use proptest::prelude::*; use super::*; @@ -1229,6 +1247,35 @@ mod tests { assert_eq!(lenient.check_path(&with_set, &EXTERNAL), Ok(None)); } + /// 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. + /// It is told apart from a prefix that is merely too long, which is + /// what the request usually comes as. + #[test] + fn a_blackhole_request_is_refused_as_one() { + let mut attributes = path(&[3333, 2121]); + attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(vec![ + (3333, 100), + (0xFFFF, 0x029A), + (0xFFFF, 0xFF01), + ]))); + assert_eq!( + ImportPolicy::default().check_path(&attributes, &EXTERNAL), + Err(Rejection::Blackhole) + ); + assert_eq!(Rejection::Blackhole.as_str(), "blackhole request"); + // Other communities are nothing to the policy. + attributes.pop(); + attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(vec![ + (3333, 666), + (0xFFFF, 0xFF01), + ]))); + assert_eq!( + ImportPolicy::default().check_path(&attributes, &EXTERNAL), + Ok(Some(2121)) + ); + } + /// What we originate is ours: a more specific route for part of it /// from another network would draw our own traffic away. #[test] diff --git a/docs/content/docs/guides/import-policy.md b/docs/content/docs/guides/import-policy.md index 3bcbfce..655e0e4 100644 --- a/docs/content/docs/guides/import-policy.md +++ b/docs/content/docs/guides/import-policy.md @@ -56,6 +56,23 @@ disabled. | 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 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` | + +### Blackhole Requests + +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 +other networks discard traffic to the address. At an Internet exchange, the +route server distributes such routes to its members with the exchange's +blackhole address as the next hop. These routes are usually host routes +(/32 or /128). + +Ferrix does not support blackhole routing. It rejects every route with the +BLACKHOLE community from an EBGP neighbor, with the reason +`blackhole request`, regardless of the prefix length. A route with the +BLACKHOLE community that is accepted as an ordinary route directs traffic +to the blackhole next hop, which discards it, so Ferrix does not accept +these routes as ordinary routes either. ### Next Hop Check @@ -105,8 +122,9 @@ Ferrix. Ferrix also applies the following checks, which the guide does not list, by default: the next hop must be the neighbor, routes for local prefixes -are rejected, AS paths with an AS_SET are rejected, and the AS path must -begin with the neighbor's AS number. +are rejected, AS paths with an AS_SET are rejected, the AS path must begin +with the neighbor's AS number, and routes with the BLACKHOLE community are +rejected. > [!NOTE] > Ferrix does not implement the default behavior of RFC 8212 on import: it diff --git a/docs/content/docs/reference/rfcs.md b/docs/content/docs/reference/rfcs.md index 7a4bdd0..1cb5de1 100644 --- a/docs/content/docs/reference/rfcs.md +++ b/docs/content/docs/reference/rfcs.md @@ -173,6 +173,14 @@ The base protocol specification. | `LARGE_COMMUNITIES` path attribute (parse, serialise) | ✅ | | Attach to locally originated routes | ✅ | +### RFC 7999 — BLACKHOLE Community + + +| Feature | Status | +|---------|--------| +| `BLACKHOLE` accepted as a community name in configuration | ✅ | +| Routes with BLACKHOLE from EBGP neighbors rejected on import (`blackhole request`) | ✅ | +| Blackhole routing: discarding traffic for prefixes with BLACKHOLE | ❌ | ## Other Capabilities (Wire Format Only)