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

+31
View File
@@ -66,6 +66,9 @@ pub struct PeerConfig {
pub import: ImportPolicy,
/// What the peer is to us, if that was said.
pub role: Option<PeerRole>,
/// Refuse the peer if it does not say what it is itself (RFC 9234
/// §4.2, "strict mode").
pub require_role: bool,
}
impl Config {
@@ -109,6 +112,10 @@ impl Config {
problem,
};
let peer = parse_peer(peer).map_err(rejected)?;
// RFC 9234 is about what networks are to each other.
if peer.role.is_some() && peer.remote_asn == proto.local_asn {
return Err(rejected(PeerProblem::RoleInOwnAs));
}
// A peer is told from the others by its name in what the
// dialer reports, and by its address when it connects to us.
if peers.iter().any(|earlier| earlier.name == peer.name) {
@@ -236,6 +243,9 @@ fn parse_peer(peer: Peer) -> Result<PeerConfig, PeerProblem> {
Ok(proto::Role::ROUTE_SERVER_CLIENT) => Some(PeerRole::RouteServerClient),
Err(_) => return Err(PeerProblem::Role),
};
if peer.require_role && role.is_none() {
return Err(PeerProblem::RequireRoleWithoutRole);
}
Ok(PeerConfig {
// A peer without a name goes by its address.
@@ -252,6 +262,7 @@ fn parse_peer(peer: Peer) -> Result<PeerConfig, PeerProblem> {
add_path,
import,
role,
require_role: peer.require_role,
})
}
@@ -764,6 +775,26 @@ mod tests {
role_of("role: ROUTE_SERVER").unwrap(),
Some(PeerRole::RouteServer)
);
// Roles are between networks, and a peer can be required to say
// its own only where we know what to expect.
let problem = |body: &str| match role_of(body).err().unwrap() {
ConfigError::Peer { problem, .. } => problem,
other => panic!("{other:?}"),
};
assert_eq!(
problem("role: PEER remote_asn: 65000"),
PeerProblem::RoleInOwnAs
);
assert_eq!(
problem("require_role: true"),
PeerProblem::RequireRoleWithoutRole
);
let config = parse(&format!(
"{BASE}peers {{ endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 \
role: PEER require_role: true }}"
))
.unwrap();
assert!(config.peers[0].require_role);
assert!(!import_of("").unwrap().accept_aspa_invalid);
assert!(
import_of("import { accept_aspa_invalid: true }")