From 3609451bdceee0c1c0dd6f830684278f26070cbb Mon Sep 17 00:00:00 2001 From: Rayhaan Jaufeerally Date: Mon, 5 Oct 2026 16:11:53 +0200 Subject: [PATCH] Ignore our own prefixes from our own routers, and say why a route for a network of this machine is held Two routers of an AS that both originate a prefix (anycast) each announce it to the other over their internal session. Each took the other's route, could not install it in the kernel, since the prefix is on one of its own interfaces, and so held it, counted as rejected with "next hop unreachable": a permanent rejection on a healthy session, for a next hop that was in fact fine. A route from a peer in our own AS for a prefix this router originates is now ignored: not taken, not held, not counted. Only that prefix itself; what is inside it is judged as usual. From other networks such routes are refused as our own prefixes, as before. A route that is held because its prefix is a network of this machine, or inside one (an exchange's LAN prefix, say), is now counted as "prefix is a network of this router", apart from routes whose next hop cannot be reached. Co-Authored-By: Claude Opus 5.5 --- crates/server/src/config.rs | 32 ++++++++++++++++- crates/server/src/policy.rs | 18 ++++++++-- crates/server/src/reach.rs | 43 ++++++++++++++++++++++- crates/server/src/routes.rs | 31 ++++++++++++---- crates/server/src/rpki.rs | 11 +++--- docs/content/docs/guides/import-policy.md | 6 ++++ docs/content/docs/guides/kernel.md | 4 ++- scripts/interop/lab.sh | 2 +- 8 files changed, 131 insertions(+), 16 deletions(-) diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs index 4504079..0a4a5ba 100644 --- a/crates/server/src/config.rs +++ b/crates/server/src/config.rs @@ -177,12 +177,17 @@ impl Config { } // What we originate is not taken from other networks. let mut own_prefixes = PrefixSet::default(); + let mut originated = PrefixSet::default(); for route in &originate { own_prefixes.allow(route.prefix, route.prefix.address_family().bits()); + originated.allow(route.prefix, route.prefix.length()); } let own_prefixes = Arc::new(own_prefixes); + let originated = Arc::new(originated); for peer in &mut peers { - if peer.remote_asn != proto.local_asn { + if peer.remote_asn == proto.local_asn { + peer.import.originated = Arc::clone(&originated); + } else { peer.import.own_prefixes = Arc::clone(&own_prefixes); } } @@ -442,6 +447,7 @@ fn parse_import(import: &proto::ImportPolicy) -> Result, + /// The prefixes this router originates, each by itself. A peer in our + /// own AS that announces one of them originates it too, as with an + /// anycast prefix that both announce: its route is of no use here, + /// where the prefix is local, and is ignored, neither taken nor + /// counted as rejected. Empty for a peer in another AS, whose routes + /// for them `own_prefixes` refuses. + pub originated: Arc, /// Communities taken off and put on every route that is taken. pub marks: Marks, /// The LOCAL_PREF every route that is taken is given; `None` leaves @@ -126,6 +133,7 @@ impl Default for ImportPolicy { accept_as_sets: false, accept_third_party_next_hop: false, own_prefixes: Arc::default(), + originated: Arc::default(), marks: Marks::default(), local_pref: None, } @@ -234,6 +242,10 @@ pub enum Rejection { /// The next hop cannot be reached from this machine as its interfaces /// are now, so the kernel could not be given the route. NextHopUnreachable, + /// The prefix is a network of one of this machine's interfaces, or + /// inside one: the machine reaches it directly, and a route by way of + /// a peer would compete with that. + OwnNetwork, } impl Rejection { @@ -243,11 +255,11 @@ impl Rejection { pub(crate) fn may_pass(self) -> bool { matches!( self, - Self::RpkiInvalid | Self::AspaInvalid | Self::NextHopUnreachable + Self::RpkiInvalid | Self::AspaInvalid | Self::NextHopUnreachable | Self::OwnNetwork ) } - pub const ALL: [Self; 18] = [ + pub const ALL: [Self; 19] = [ Self::BogonPrefix, Self::TooShort, Self::TooSpecific, @@ -266,6 +278,7 @@ impl Rejection { Self::NextHopNotPeer, Self::OwnPrefix, Self::NextHopUnreachable, + Self::OwnNetwork, ]; /// Where the reason is in [`Rejection::ALL`]. @@ -293,6 +306,7 @@ impl Rejection { Self::NextHopNotPeer => "next hop is not the peer", Self::OwnPrefix => "prefix is our own", Self::NextHopUnreachable => "next hop unreachable", + Self::OwnNetwork => "prefix is a network of this router", } } } diff --git a/crates/server/src/reach.rs b/crates/server/src/reach.rs index 3ce757b..89286a1 100644 --- a/crates/server/src/reach.rs +++ b/crates/server/src/reach.rs @@ -213,7 +213,7 @@ mod tests { use bgp_rib::table::PeerInfo; use super::*; - use crate::policy::{ImportPolicy, Rejection, RouteSource}; + use crate::policy::{ImportPolicy, PrefixSet, Rejection, RouteSource}; use crate::refusals::RefusalLog; use crate::routes::{self, Held, Import}; use crate::rpki::Checks; @@ -354,6 +354,47 @@ mod tests { } } + /// A route for a network of this machine, or for part of one, is held + /// under a reason of its own, not as one whose next hop is out of + /// reach: the next hop is fine, the prefix is the trouble. + #[test] + fn a_route_for_a_network_of_this_machine_is_held_as_such() { + let bed = Bed::new(); + let mut peer = bed.peer("192.0.2.2", 65001); + let interfaces = interfaces(false); + bed.announce(&mut peer, &interfaces, "192.0.2.0/24", 1); + bed.announce(&mut peer, &interfaces, "192.0.2.128/25", 1); + assert_eq!((bed.table.len(), peer.held.len()), (0, 2)); + let refused = bed.refusals.peers(); + assert_eq!(refused[0].1.by_reason(), [(Rejection::OwnNetwork, 2)]); + } + + /// A peer in our own AS that originates a prefix we originate too (an + /// anycast prefix) has its route for it ignored: not taken, not held, + /// not counted as refused. What it had announced before goes. + #[test] + fn a_prefix_originated_here_too_is_ignored() { + let mut bed = Bed::new(); + let mut peer = bed.peer("192.0.2.2", 65001); + let interfaces = interfaces(false); + bed.announce(&mut peer, &interfaces, "203.0.113.0/24", 1); + assert_eq!(bed.table.len(), 1); + + let mut originated = PrefixSet::default(); + originated.allow(prefix("203.0.113.0/24"), 24); + bed.policy.originated = Arc::new(originated); + bed.announce(&mut peer, &interfaces, "203.0.113.0/24", 1); + assert_eq!((bed.table.len(), peer.held.len()), (0, 0)); + assert!( + bed.refusals.peers().is_empty(), + "nothing counted as refused" + ); + + // Only the prefix itself: what is inside it is judged as usual. + bed.announce(&mut peer, &interfaces, "203.0.113.0/25", 1); + assert_eq!(bed.table.len(), 1); + } + /// RFC 4271 ยง9.1.2.1: a route whose next hop cannot be reached is not /// chosen, however good it looks; and it is when it can be. #[test] diff --git a/crates/server/src/routes.rs b/crates/server/src/routes.rs index fd1e10c..c047fe6 100644 --- a/crates/server/src/routes.rs +++ b/crates/server/src/routes.rs @@ -240,6 +240,13 @@ impl Applying<'_> { let mut accepted = Cow::Borrowed(nlri); let mut invalid = Vec::new(); for (at, route) in nlri.iter().enumerate() { + if self.import.policy.originated.allows(&route.prefix) { + if let Cow::Borrowed(_) = accepted { + accepted = Cow::Owned(nlri[..at].to_vec()); + } + self.ignore(route); + continue; + } match self.judge(route, origin, near) { Ok(()) => { if let Cow::Owned(accepted) = &mut accepted { @@ -318,17 +325,29 @@ impl Applying<'_> { { return Err(Rejection::AspaInvalid); } - let reachable = near - && !import - .checks - .next_hops - .is_some_and(|next_hops| next_hops.is_own_network(&route.prefix)); - if !reachable { + if import + .checks + .next_hops + .is_some_and(|next_hops| next_hops.is_own_network(&route.prefix)) + { + return Err(Rejection::OwnNetwork); + } + if !near { return Err(Rejection::NextHopUnreachable); } Ok(()) } + /// Takes no notice of `route`, a prefix this router originates itself, + /// from a peer in our own AS (see `ImportPolicy::originated`): it is + /// not taken, and not counted as refused. What the peer had announced + /// for it before is taken out. + fn ignore(&mut self, route: &Nlri) { + self.applied.outcome += self + .table + .withdraw(self.import.peer, std::slice::from_ref(route)); + } + /// Notes that `refused`, whose next hop is `next_hop` if they came in /// MP_REACH_NLRI, were not taken, and takes out what the peer had /// announced for them before. diff --git a/crates/server/src/rpki.rs b/crates/server/src/rpki.rs index 57e4e1a..bf7e457 100644 --- a/crates/server/src/rpki.rs +++ b/crates/server/src/rpki.rs @@ -158,10 +158,13 @@ impl<'a> Checks<'a> { return Some(Rejection::AspaInvalid); } } - if let Some(next_hops) = self.next_hops - && !next_hops.reaches(prefix, attrs.next_hop().as_ref()) - { - return Some(Rejection::NextHopUnreachable); + if let Some(next_hops) = self.next_hops { + if next_hops.is_own_network(prefix) { + return Some(Rejection::OwnNetwork); + } + if !next_hops.reaches(prefix, attrs.next_hop().as_ref()) { + return Some(Rejection::NextHopUnreachable); + } } None } diff --git a/docs/content/docs/guides/import-policy.md b/docs/content/docs/guides/import-policy.md index 655e0e4..c5154ff 100644 --- a/docs/content/docs/guides/import-policy.md +++ b/docs/content/docs/guides/import-policy.md @@ -48,6 +48,12 @@ For IBGP neighbors (where `remote_asn` equals `local_asn`), Ferrix applies only the checks on the prefix. The AS path checks do not apply, and the next hop can be any usable address. +An IBGP neighbor can advertise a prefix that the local router also +originates, for example an anycast prefix that both routers originate. +Ferrix ignores such a route: it does not accept the route, and does not +count it as rejected. This applies only to the originated prefix itself, +not to more specific prefixes within it. + Ferrix always rejects the routes in Table 2. These checks cannot be disabled. diff --git a/docs/content/docs/guides/kernel.md b/docs/content/docs/guides/kernel.md index 39f2530..23a3c70 100644 --- a/docs/content/docs/guides/kernel.md +++ b/docs/content/docs/guides/kernel.md @@ -78,7 +78,9 @@ kernel reaches these destinations directly. Without this rule, a neighbor on a shared LAN that advertises part of the LAN could attract the traffic for other hosts on the LAN, including the BGP sessions with them. -The status page counts such routes as "inside a network of this machine". +Ferrix holds such a route, and counts it as rejected with the reason +`prefix is a network of this router`. If the subnet is removed from the +interface, Ferrix accepts the route. ## Route Ownership diff --git a/scripts/interop/lab.sh b/scripts/interop/lab.sh index 81b15a3..72978a1 100755 --- a/scripts/interop/lab.sh +++ b/scripts/interop/lab.sh @@ -818,7 +818,7 @@ phase_hijacks() { # The LAN's own prefix. The lab takes bogons, so the policy lets it # by; it is still not a route the kernel could be given, and so not # one to take: it would compete with the connected route. - wait_for "the LAN's prefix to be held" refused_for "next hop unreachable" + wait_for "the LAN's prefix to be held" refused_for "prefix is a network of this router" dut_lacks 10.9.0.0/24 || fail "the LAN's own prefix was taken from FRR" in_ns dut ip -4 route show proto bgp | grep -q "^10.9.0.0/24 " && fail "the LAN's own prefix was installed by way of a peer" in_ns dut ping -q -c 1 -W 2 10.9.0.3 >/dev/null || fail "the LAN is not reachable any more"