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"