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:
1 parent
52593bb2c2
commit
3609451bdc
8 files changed
+131
-16
No files matched your search
@@ -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<ImportPolicy, ImportProb
|
||||
accept_as_sets: import.accept_as_sets,
|
||||
accept_third_party_next_hop: import.accept_third_party_next_hop,
|
||||
own_prefixes: Arc::default(),
|
||||
originated: Arc::default(),
|
||||
marks: Marks::parse(
|
||||
&import.remove_communities,
|
||||
&import.add_communities,
|
||||
@@ -564,6 +570,30 @@ mod tests {
|
||||
|
||||
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]
|
||||
fn originated_routes_are_parsed_with_their_attributes() {
|
||||
let config = parse(&format!(
|
||||
|
||||
@@ -99,6 +99,13 @@ pub struct ImportPolicy {
|
||||
/// a customer with a part of our space. Empty for a peer in our own
|
||||
/// AS.
|
||||
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.
|
||||
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user