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)