Check roles in OPEN and prevent route leaks with Only to Customer (RFC 9234)

A peer's configured role now does three more things:

- Our OPEN carries the BGP Role capability with what we are to the peer.
  A peer whose own says it is something that does not pair with that, or
  that sends two different roles, is refused with OPEN Message Error /
  Role Mismatch. A peer that says nothing is accepted unless require_role
  is set. A role on a peer in our own AS is a configuration error.
- Routes from a provider, a peer or a route server are marked Only to
  Customer on the way in if they are not already, and our routes are
  marked on the way out to a customer, a peer or a route server client.
- A marked route from a customer or route server client, or from a peer
  with someone else's mark, is a leak and is refused. No policy setting
  turns that off.

The packet crate gains the Only to Customer attribute (malformed:
treat-as-withdraw), and the Role Mismatch subcode is corrected from 8 to
11.

The status page shows whether the peer confirmed its role.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 14:27:36 +02:00
1 parent 8086aa994f
commit f9ef781d51
30 files changed
+1128 -25

No files matched your search

+28
View File
@@ -6,6 +6,7 @@ use std::time::Duration;
use bgp_packet::errors::{BgpError, CeaseSubcode, DecodeError, FsmSubcode, OpenMsgSubcode};
use bgp_packet::message::NotificationMessage;
use bgp_packet::open::BgpRole;
use bgp_primitives::parser::EncodeError;
use bgp_rib::RibStopped;
use bgp_rib::table::TooManyPeers;
@@ -13,6 +14,21 @@ use thiserror::Error;
use crate::family::FamilySet;
use crate::fsm::State;
use crate::policy::PeerRole;
/// Says what is wrong between what a peer is configured as and what it
/// says it is.
fn role_mismatch(configured: Option<PeerRole>, theirs: Option<BgpRole>) -> String {
let configured = configured.map_or("no role is configured".to_string(), |role| {
format!("the peer is configured as our {}", role.as_str())
});
match theirs {
Some(theirs) => format!("role mismatch: {configured} and says it is {theirs:?}"),
None => format!(
"role mismatch: {configured} and does not say what it is, or says more than one thing"
),
}
}
/// Why a session ended abnormally.
#[derive(Debug, Error)]
@@ -40,6 +56,13 @@ pub enum SessionError {
OpenRefused(BgpError),
#[error("peer announced AS{actual} but AS{expected} was configured")]
WrongPeerAs { expected: u32, actual: u32 },
/// RFC 9234 §4.2. `theirs` is `None` if the peer did not say, or said
/// more than one thing.
#[error("{}", role_mismatch(*.configured, *.theirs))]
RoleMismatch {
configured: Option<PeerRole>,
theirs: Option<BgpRole>,
},
#[error("closed by connection collision resolution")]
Collision,
/// The routing table has no room for another peer.
@@ -88,6 +111,7 @@ impl SessionError {
}),
Self::OpenRefused(error) => error.clone(),
Self::WrongPeerAs { .. } => BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs),
Self::RoleMismatch { .. } => BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch),
Self::Collision => BgpError::Cease(CeaseSubcode::ConnCollisionResolution),
Self::TableFull(_) => BgpError::Cease(CeaseSubcode::OutOfResources),
Self::PrefixLimitExceeded { .. } => BgpError::Cease(CeaseSubcode::MaxPrefixes),
@@ -179,6 +203,10 @@ pub enum PeerProblem {
AddressFamily,
#[error("add_path has a value that is not one of its settings")]
AddPath,
#[error("role is set for a peer in our own AS; roles are between networks")]
RoleInOwnAs,
#[error("require_role is set without a role")]
RequireRoleWithoutRole,
#[error("role has a value that is not one of its settings")]
Role,
#[error("import: {0}")]