Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
instance probe transitions from NotReady to Ready only after both links are
owned-safe. SVD / collect-metadata VXLAN plus VRF/L3VXLAN lifecycle stay
deferred.
- **ADR-0091 managed VRF/L3VXLAN schema and status substrate.**
`[managed_netdevs]` now accepts `[[managed_netdevs.vrfs]]` and
`[[managed_netdevs.l3vxlans]]` rows, derives
`rustbgpd:vrf:<owner>:<name>` and `rustbgpd:l3vxlan:<owner>:<name>`
ownership stamps, validates protected VRF/L3VXLAN identity attributes,
parses observed VRF/L3VXLAN link state from Linux link dumps, and exposes
desired/observed/orphan/foreign/unsafe status through
`EvpnService.ListManagedNetdevs` and `rbgp evpn managed-netdevs`. VRF and
L3VXLAN create/adopt/reap lifecycle remains deferred to the next managed
netdev slice.

### Changed

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,9 @@ See [docs/INTEROP.md](docs/INTEROP.md) for full procedures and results.
topologies, including SVD / collect-metadata VXLAN; opt-in bridge and
fixed-VNI VXLAN netdev creation now ship under
[ADR-0091](docs/adr/0091-evpn-managed-netdev-creation.md)
(`[managed_netdevs]`). SVD / collect-metadata VXLAN and VRF netdev
creation remain operator-provisioned per
(`[managed_netdevs]`), and VRF/L3VXLAN rows now have schema validation,
ownership stamps, and `ListManagedNetdevs` status. SVD / collect-metadata
VXLAN and VRF/L3VXLAN lifecycle creation remain operator-provisioned per
[ADR-0088](docs/adr/0088-evpn-vlan-aware-bridge-managed-netdev-boundary.md);
[ADR-0089](docs/adr/0089-evpn-vni-per-bd-vlan-aware-bridge-support.md)
scopes the first VLAN-aware bridge support to VNI-per-broadcast-domain
Expand Down Expand Up @@ -412,7 +413,7 @@ evolving API.**
| **Runtime** | Rust 1.95+ (workspace MSRV — set by the bundled SQLite build), single binary, no external dependencies except optional RPKI/BMP/MRT backends |
| **Config stability** | TOML format may change between minor versions; migrations documented in CHANGELOG |
| **API stability** | gRPC proto may add fields/RPCs; breaking changes documented in CHANGELOG |
| **Not yet supported** | EVPN runtime L3VNI/device/table IP-VRF identity changes (restart-required by design) and ES/IP-VRF row mixed edits outside the L2VNI-only composer, true RFC VLAN-aware bundle / non-zero Ethernet Tag service, rustbgpd-managed SVD / collect-metadata VXLAN and VRF / L3VXLAN netdev creation (managed bridge and fixed-VNI VXLAN creation now ship), EVPN route types 6-11 / PBB / MVPN / MPLS/SRv6 service encapsulation, VPNv4/v6, labeled-unicast, Route Target Constraints, BGP-LS, Confederation, TCP-AO dynamic-neighbor / runtime-rotation / multi-key rollover |
| **Not yet supported** | EVPN runtime L3VNI/device/table IP-VRF identity changes (restart-required by design) and ES/IP-VRF row mixed edits outside the L2VNI-only composer, true RFC VLAN-aware bundle / non-zero Ethernet Tag service, rustbgpd-managed SVD / collect-metadata VXLAN and VRF / L3VXLAN lifecycle creation (managed bridge and fixed-VNI VXLAN lifecycle now ship; VRF/L3VXLAN schema/status now ship), EVPN route types 6-11 / PBB / MVPN / MPLS/SRv6 service encapsulation, VPNv4/v6, labeled-unicast, Route Target Constraints, BGP-LS, Confederation, TCP-AO dynamic-neighbor / runtime-rotation / multi-key rollover |
| **Tests** | Workspace test suite, fuzz targets, an automated interop suite (see `docs/INTEROP.md`) primarily against FRR plus GoBGP / StayRTR / documented BIRD coverage, and an in-tree EVPN load generator (foundation tier gated on every PR; privileged kernel dataplane smokes run on GitHub-hosted CI) |

## Documentation
Expand Down
19 changes: 14 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,14 @@ has it, no broad performance sprints without profile evidence.
vnifilter mode the fixed-VNI lifecycle never creates;
`managed_ready` proves that a rustbgpd-created bridge plus rustbgpd-created
fixed-VNI VXLAN make the real EVPN L2 instance probe Ready only after both
links are owned-safe; SVD / collect-metadata VXLAN and VRF/L3VXLAN classes
still deferred. The `svd_fdb_vni` netns proof
links are owned-safe. **ADR-0091 VRF/L3VXLAN schema/status substrate
landed:** `[managed_netdevs]` accepts VRF and L3VXLAN desired rows, derives
`rustbgpd:vrf:<owner>:<name>` and `rustbgpd:l3vxlan:<owner>:<name>` stamps,
parses observed VRF/L3VXLAN protected attributes from Linux link dumps, and
reports desired/observed/orphan/foreign/unsafe state through
`ListManagedNetdevs` / `rbgp`; VRF/L3VXLAN create/adopt/reap lifecycle
remains deferred. SVD / collect-metadata VXLAN lifecycle is still deferred.
The `svd_fdb_vni` netns proof
covers Ready + add + same-MAC two-VNI isolation + scoped delete on a real
kernel; sparse `NDA_VLAN` / `NDA_DST` echoes are handled by configured-VLAN
inference plus owned-state convergence. Service-provider EVPN breadth
Expand Down Expand Up @@ -365,9 +371,12 @@ has it, no broad performance sprints without profile evidence.
`desired-absent`, `foreign-present`, `owned-unsafe`, `owned-safe`,
`orphaned`, or `unknown`; the dataplane actor creates missing managed
bridges and fixed-VNI VXLANs, adopts exact stamped links after restart, and
safely reaps same-owner bridge/VXLAN orphans; `managed_ready` proves that
this rustbgpd-created bridge + VXLAN topology drives the real EVPN L2 probe
to Ready. A dedicated counter
safely reaps same-owner bridge/VXLAN orphans. VRF/L3VXLAN schema and status
substrate now ship too: desired rows validate, derive ownership stamps, parse
observed VRF/L3VXLAN link attributes, and surface status through
`ListManagedNetdevs` / `rbgp`, while VRF/L3VXLAN create/adopt/reap lifecycle
remains deferred. `managed_ready` proves that this rustbgpd-created bridge +
VXLAN topology drives the real EVPN L2 probe to Ready. A dedicated counter
for unattributable-VLAN local-MAC
classifier misses is intentionally not a feature: those events fail closed as
normal "not ours" outcomes, while downstream originator backpressure is
Expand Down
195 changes: 152 additions & 43 deletions crates/api/src/evpn_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,8 @@ fn managed_netdev_to_proto(row: &ManagedNetdevStatus) -> proto::ManagedNetdevSta
class: match row.class {
rustbgpd_evpn::ManagedNetdevClass::Bridge => proto::ManagedNetdevClass::Bridge as i32,
rustbgpd_evpn::ManagedNetdevClass::Vxlan => proto::ManagedNetdevClass::Vxlan as i32,
rustbgpd_evpn::ManagedNetdevClass::Vrf => proto::ManagedNetdevClass::Vrf as i32,
rustbgpd_evpn::ManagedNetdevClass::L3Vxlan => proto::ManagedNetdevClass::L3vxlan as i32,
},
name: row.name.clone(),
desired: row.desired,
Expand Down Expand Up @@ -926,6 +928,10 @@ fn managed_netdev_to_proto(row: &ManagedNetdevStatus) -> proto::ManagedNetdevSta
observed_collect_metadata: row.observed_collect_metadata,
observed_vnifilter: row.observed_vnifilter,
observed_bridge: row.observed_bridge.clone(),
observed_table_id: row.observed_table_id,
observed_up: row.observed_up,
observed_master: row.observed_master.clone(),
observed_router_mac: row.observed_router_mac.map(|mac| mac.to_string()),
}
}

Expand Down Expand Up @@ -2136,55 +2142,131 @@ mod tests {
#[tokio::test]
async fn list_managed_netdevs_reads_status_snapshot() {
let svc = EvpnService::new(Arc::new(EvpnInstanceTable::new()))
.with_managed_netdev_status_snapshot(Arc::new(|| {
vec![
ManagedNetdevStatus {
class: rustbgpd_evpn::ManagedNetdevClass::Bridge,
name: "br100".to_string(),
desired: true,
ownership_stamp: Some("rustbgpd:bridge:leaf-1:br100".to_string()),
state: rustbgpd_evpn::ManagedNetdevState::OwnedSafe,
reason: String::new(),
ifindex: Some(10),
observed_vlan_filtering: Some(false),
observed_vni: None,
observed_local_ip: None,
observed_dstport: None,
observed_learning_disabled: None,
observed_collect_metadata: None,
observed_vnifilter: None,
observed_bridge: None,
observed_stamps: vec!["rustbgpd:bridge:leaf-1:br100".to_string()],
},
ManagedNetdevStatus {
class: rustbgpd_evpn::ManagedNetdevClass::Vxlan,
name: "vxlan100".to_string(),
desired: true,
ownership_stamp: Some("rustbgpd:vxlan:leaf-1:vxlan100".to_string()),
state: rustbgpd_evpn::ManagedNetdevState::OwnedSafe,
reason: String::new(),
ifindex: Some(20),
observed_vlan_filtering: None,
observed_vni: Some(100),
observed_local_ip: Some("10.0.0.1".parse().unwrap()),
observed_dstport: Some(4789),
observed_learning_disabled: Some(true),
observed_collect_metadata: Some(false),
observed_vnifilter: Some(false),
observed_bridge: Some("br100".to_string()),
observed_stamps: vec!["rustbgpd:vxlan:leaf-1:vxlan100".to_string()],
},
]
}));
.with_managed_netdev_status_snapshot(Arc::new(managed_netdev_status_fixture));

let resp = svc
.list_managed_netdevs(Request::new(proto::ListManagedNetdevsRequest {}))
.await
.unwrap()
.into_inner();

assert_eq!(resp.netdevs.len(), 2);
let row = &resp.netdevs[0];
assert_eq!(resp.netdevs.len(), 4);
assert_bridge_managed_netdev(&resp.netdevs[0]);
assert_vxlan_managed_netdev(&resp.netdevs[1]);
assert_vrf_managed_netdev(&resp.netdevs[2]);
assert_l3vxlan_managed_netdev(&resp.netdevs[3]);
}

fn managed_netdev_status_fixture() -> Vec<ManagedNetdevStatus> {
vec![
bridge_managed_netdev_status(),
vxlan_managed_netdev_status(),
vrf_managed_netdev_status(),
l3vxlan_managed_netdev_status(),
]
}

fn bridge_managed_netdev_status() -> ManagedNetdevStatus {
ManagedNetdevStatus {
class: rustbgpd_evpn::ManagedNetdevClass::Bridge,
name: "br100".to_string(),
desired: true,
ownership_stamp: Some("rustbgpd:bridge:leaf-1:br100".to_string()),
state: rustbgpd_evpn::ManagedNetdevState::OwnedSafe,
reason: String::new(),
ifindex: Some(10),
observed_vlan_filtering: Some(false),
observed_vni: None,
observed_local_ip: None,
observed_dstport: None,
observed_learning_disabled: None,
observed_collect_metadata: None,
observed_vnifilter: None,
observed_bridge: None,
observed_table_id: None,
observed_up: None,
observed_master: None,
observed_router_mac: None,
observed_stamps: vec!["rustbgpd:bridge:leaf-1:br100".to_string()],
}
}

fn vxlan_managed_netdev_status() -> ManagedNetdevStatus {
ManagedNetdevStatus {
class: rustbgpd_evpn::ManagedNetdevClass::Vxlan,
name: "vxlan100".to_string(),
desired: true,
ownership_stamp: Some("rustbgpd:vxlan:leaf-1:vxlan100".to_string()),
state: rustbgpd_evpn::ManagedNetdevState::OwnedSafe,
reason: String::new(),
ifindex: Some(20),
observed_vlan_filtering: None,
observed_vni: Some(100),
observed_local_ip: Some("10.0.0.1".parse().unwrap()),
observed_dstport: Some(4789),
observed_learning_disabled: Some(true),
observed_collect_metadata: Some(false),
observed_vnifilter: Some(false),
observed_bridge: Some("br100".to_string()),
observed_table_id: None,
observed_up: None,
observed_master: None,
observed_router_mac: None,
observed_stamps: vec!["rustbgpd:vxlan:leaf-1:vxlan100".to_string()],
}
}

fn vrf_managed_netdev_status() -> ManagedNetdevStatus {
ManagedNetdevStatus {
class: rustbgpd_evpn::ManagedNetdevClass::Vrf,
name: "vrf100".to_string(),
desired: true,
ownership_stamp: Some("rustbgpd:vrf:leaf-1:vrf100".to_string()),
state: rustbgpd_evpn::ManagedNetdevState::OwnedSafe,
reason: String::new(),
ifindex: Some(30),
observed_vlan_filtering: None,
observed_vni: None,
observed_local_ip: None,
observed_dstport: None,
observed_learning_disabled: None,
observed_collect_metadata: None,
observed_vnifilter: None,
observed_bridge: None,
observed_table_id: Some(5000),
observed_up: Some(true),
observed_master: None,
observed_router_mac: None,
observed_stamps: vec!["rustbgpd:vrf:leaf-1:vrf100".to_string()],
}
}

fn l3vxlan_managed_netdev_status() -> ManagedNetdevStatus {
ManagedNetdevStatus {
class: rustbgpd_evpn::ManagedNetdevClass::L3Vxlan,
name: "l3vxlan100".to_string(),
desired: true,
ownership_stamp: Some("rustbgpd:l3vxlan:leaf-1:l3vxlan100".to_string()),
state: rustbgpd_evpn::ManagedNetdevState::OwnedSafe,
reason: String::new(),
ifindex: Some(40),
observed_vlan_filtering: None,
observed_vni: Some(5000),
observed_local_ip: Some("10.0.0.1".parse().unwrap()),
observed_dstport: Some(4789),
observed_learning_disabled: Some(true),
observed_collect_metadata: Some(false),
observed_vnifilter: Some(false),
observed_bridge: None,
observed_table_id: None,
observed_up: Some(true),
observed_master: Some("vrf100".to_string()),
observed_router_mac: Some(MacAddress::new([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])),
observed_stamps: vec!["rustbgpd:l3vxlan:leaf-1:l3vxlan100".to_string()],
}
}

fn assert_bridge_managed_netdev(row: &proto::ManagedNetdevState) {
assert_eq!(row.class, proto::ManagedNetdevClass::Bridge as i32);
assert_eq!(row.name, "br100");
assert!(row.desired);
Expand All @@ -2196,7 +2278,9 @@ mod tests {
assert_eq!(row.ifindex, Some(10));
assert_eq!(row.observed_vlan_filtering, Some(false));
assert_eq!(row.observed_stamps, vec!["rustbgpd:bridge:leaf-1:br100"]);
let vxlan = &resp.netdevs[1];
}

fn assert_vxlan_managed_netdev(vxlan: &proto::ManagedNetdevState) {
assert_eq!(vxlan.class, proto::ManagedNetdevClass::Vxlan as i32);
assert_eq!(vxlan.name, "vxlan100");
assert_eq!(vxlan.ownership_stamp, "rustbgpd:vxlan:leaf-1:vxlan100");
Expand All @@ -2210,6 +2294,31 @@ mod tests {
assert_eq!(vxlan.observed_bridge.as_deref(), Some("br100"));
}

fn assert_vrf_managed_netdev(vrf: &proto::ManagedNetdevState) {
assert_eq!(vrf.class, proto::ManagedNetdevClass::Vrf as i32);
assert_eq!(vrf.name, "vrf100");
assert_eq!(vrf.ownership_stamp, "rustbgpd:vrf:leaf-1:vrf100");
assert_eq!(vrf.ifindex, Some(30));
assert_eq!(vrf.observed_table_id, Some(5000));
assert_eq!(vrf.observed_up, Some(true));
}

fn assert_l3vxlan_managed_netdev(l3vxlan: &proto::ManagedNetdevState) {
assert_eq!(l3vxlan.class, proto::ManagedNetdevClass::L3vxlan as i32);
assert_eq!(l3vxlan.name, "l3vxlan100");
assert_eq!(
l3vxlan.ownership_stamp,
"rustbgpd:l3vxlan:leaf-1:l3vxlan100"
);
assert_eq!(l3vxlan.ifindex, Some(40));
assert_eq!(l3vxlan.observed_vni, Some(5000));
assert_eq!(l3vxlan.observed_master.as_deref(), Some("vrf100"));
assert_eq!(
l3vxlan.observed_router_mac.as_deref(),
Some("02:00:00:00:00:01")
);
}

// -- Gate 9 IP-VRF surface --------------------------------------

use rustbgpd_evpn::IpVrfDataplaneStatus;
Expand Down
Loading
Loading