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 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Opus 5.5 committed 2026-10-05 16:11:53 +02:00
1 parent 52593bb2c2
commit 3609451bdc
8 files changed
+131 -16

No files matched your search

+31 -1
View File
@@ -177,12 +177,17 @@ impl Config {
} }
// What we originate is not taken from other networks. // What we originate is not taken from other networks.
let mut own_prefixes = PrefixSet::default(); let mut own_prefixes = PrefixSet::default();
let mut originated = PrefixSet::default();
for route in &originate { for route in &originate {
own_prefixes.allow(route.prefix, route.prefix.address_family().bits()); 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 own_prefixes = Arc::new(own_prefixes);
let originated = Arc::new(originated);
for peer in &mut peers { 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); peer.import.own_prefixes = Arc::clone(&own_prefixes);
} }
} }
@@ -442,6 +447,7 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
accept_as_sets: import.accept_as_sets, accept_as_sets: import.accept_as_sets,
accept_third_party_next_hop: import.accept_third_party_next_hop, accept_third_party_next_hop: import.accept_third_party_next_hop,
own_prefixes: Arc::default(), own_prefixes: Arc::default(),
originated: Arc::default(),
marks: Marks::parse( marks: Marks::parse(
&import.remove_communities, &import.remove_communities,
&import.add_communities, &import.add_communities,
@@ -564,6 +570,30 @@ mod tests {
const BASE: &str = "router_id: \"192.0.2.1\" local_asn: 65000\n"; const BASE: &str = "router_id: \"192.0.2.1\" local_asn: 65000\n";
/// What we originate is refused from other networks, with what is
/// inside it; from a router of our own it is ignored, and only the
/// prefix itself.
#[test]
fn originated_prefixes_are_ours_or_shared_by_where_a_route_comes_from() {
let config = parse(&format!(
"{BASE}originate {{ prefix: \"203.0.113.0/24\" }}
peers {{ name: \"external\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 64496 }}
peers {{ name: \"internal\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 65000 }}"
))
.unwrap();
let ours = IpPrefix::try_from("203.0.113.0/24").unwrap();
let inside = IpPrefix::try_from("203.0.113.0/25").unwrap();
let [external, internal] = &config.peers[..] else {
panic!("two peers");
};
assert!(external.import.own_prefixes.allows(&ours));
assert!(external.import.own_prefixes.allows(&inside));
assert!(external.import.originated.is_empty());
assert!(internal.import.own_prefixes.is_empty());
assert!(internal.import.originated.allows(&ours));
assert!(!internal.import.originated.allows(&inside));
}
#[test] #[test]
fn originated_routes_are_parsed_with_their_attributes() { fn originated_routes_are_parsed_with_their_attributes() {
let config = parse(&format!( let config = parse(&format!(
+16 -2
View File
@@ -99,6 +99,13 @@ pub struct ImportPolicy {
/// a customer with a part of our space. Empty for a peer in our own /// a customer with a part of our space. Empty for a peer in our own
/// AS. /// AS.
pub own_prefixes: Arc<PrefixSet>, pub own_prefixes: Arc<PrefixSet>,
/// 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<PrefixSet>,
/// Communities taken off and put on every route that is taken. /// Communities taken off and put on every route that is taken.
pub marks: Marks, pub marks: Marks,
/// The LOCAL_PREF every route that is taken is given; `None` leaves /// 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_as_sets: false,
accept_third_party_next_hop: false, accept_third_party_next_hop: false,
own_prefixes: Arc::default(), own_prefixes: Arc::default(),
originated: Arc::default(),
marks: Marks::default(), marks: Marks::default(),
local_pref: None, local_pref: None,
} }
@@ -234,6 +242,10 @@ pub enum Rejection {
/// The next hop cannot be reached from this machine as its interfaces /// The next hop cannot be reached from this machine as its interfaces
/// are now, so the kernel could not be given the route. /// are now, so the kernel could not be given the route.
NextHopUnreachable, 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 { impl Rejection {
@@ -243,11 +255,11 @@ impl Rejection {
pub(crate) fn may_pass(self) -> bool { pub(crate) fn may_pass(self) -> bool {
matches!( matches!(
self, 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::BogonPrefix,
Self::TooShort, Self::TooShort,
Self::TooSpecific, Self::TooSpecific,
@@ -266,6 +278,7 @@ impl Rejection {
Self::NextHopNotPeer, Self::NextHopNotPeer,
Self::OwnPrefix, Self::OwnPrefix,
Self::NextHopUnreachable, Self::NextHopUnreachable,
Self::OwnNetwork,
]; ];
/// Where the reason is in [`Rejection::ALL`]. /// Where the reason is in [`Rejection::ALL`].
@@ -293,6 +306,7 @@ impl Rejection {
Self::NextHopNotPeer => "next hop is not the peer", Self::NextHopNotPeer => "next hop is not the peer",
Self::OwnPrefix => "prefix is our own", Self::OwnPrefix => "prefix is our own",
Self::NextHopUnreachable => "next hop unreachable", Self::NextHopUnreachable => "next hop unreachable",
Self::OwnNetwork => "prefix is a network of this router",
} }
} }
} }
+42 -1
View File
@@ -213,7 +213,7 @@ mod tests {
use bgp_rib::table::PeerInfo; use bgp_rib::table::PeerInfo;
use super::*; use super::*;
use crate::policy::{ImportPolicy, Rejection, RouteSource}; use crate::policy::{ImportPolicy, PrefixSet, Rejection, RouteSource};
use crate::refusals::RefusalLog; use crate::refusals::RefusalLog;
use crate::routes::{self, Held, Import}; use crate::routes::{self, Held, Import};
use crate::rpki::Checks; 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 /// 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. /// chosen, however good it looks; and it is when it can be.
#[test] #[test]
+25 -6
View File
@@ -240,6 +240,13 @@ impl Applying<'_> {
let mut accepted = Cow::Borrowed(nlri); let mut accepted = Cow::Borrowed(nlri);
let mut invalid = Vec::new(); let mut invalid = Vec::new();
for (at, route) in nlri.iter().enumerate() { 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) { match self.judge(route, origin, near) {
Ok(()) => { Ok(()) => {
if let Cow::Owned(accepted) = &mut accepted { if let Cow::Owned(accepted) = &mut accepted {
@@ -318,17 +325,29 @@ impl Applying<'_> {
{ {
return Err(Rejection::AspaInvalid); return Err(Rejection::AspaInvalid);
} }
let reachable = near if import
&& !import .checks
.checks .next_hops
.next_hops .is_some_and(|next_hops| next_hops.is_own_network(&route.prefix))
.is_some_and(|next_hops| next_hops.is_own_network(&route.prefix)); {
if !reachable { return Err(Rejection::OwnNetwork);
}
if !near {
return Err(Rejection::NextHopUnreachable); return Err(Rejection::NextHopUnreachable);
} }
Ok(()) 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 /// 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 /// MP_REACH_NLRI, were not taken, and takes out what the peer had
/// announced for them before. /// announced for them before.
+7 -4
View File
@@ -158,10 +158,13 @@ impl<'a> Checks<'a> {
return Some(Rejection::AspaInvalid); return Some(Rejection::AspaInvalid);
} }
} }
if let Some(next_hops) = self.next_hops if let Some(next_hops) = self.next_hops {
&& !next_hops.reaches(prefix, attrs.next_hop().as_ref()) if next_hops.is_own_network(prefix) {
{ return Some(Rejection::OwnNetwork);
return Some(Rejection::NextHopUnreachable); }
if !next_hops.reaches(prefix, attrs.next_hop().as_ref()) {
return Some(Rejection::NextHopUnreachable);
}
} }
None None
} }
@@ -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 only the checks on the prefix. The AS path checks do not apply, and the
next hop can be any usable address. 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 Ferrix always rejects the routes in Table 2. These checks cannot be
disabled. disabled.
+3 -1
View File
@@ -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 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. 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 ## Route Ownership
+1 -1
View File
@@ -818,7 +818,7 @@ phase_hijacks() {
# The LAN's own prefix. The lab takes bogons, so the policy lets it # 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 # 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. # 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" 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 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" in_ns dut ping -q -c 1 -W 2 10.9.0.3 >/dev/null || fail "the LAN is not reachable any more"