rustbgpd
Cookbook

L3VPN route reflector (VPNv4/VPNv6 + RT-Constrain)

This recipe builds a control-plane-only VPNv4/VPNv6 route reflector.

This recipe builds a control-plane-only VPNv4/VPNv6 route reflector.

When this is you: Your PE fleet exchanges VPNv4/VPNv6 routes. These use SAFI 128, and you want the reflector out of the vendor-appliance business — reflect with RD, MPLS label stack, next-hop, and Route Targets preserved verbatim, and only send each PE the routes whose RTs it actually imports (RFC 4684 RT-Constrain). Scope honesty up front: this is the route-reflector / controller-feed slice only (ADR-0077). rustbgpd does no VRF import, no MPLS label forwarding, no CE-facing attachment circuits — the PEs keep their dataplane; the RR moves routes.

Proven by: M74 (VPNv4/VPNv6 reflection over the same IPv4 sessions, with shared RDs, family/peer-scoped API and sink views, field-equal NLRI, and same-path RFC 4456 attributes vs GoBGP), M75 (RT-Constrain filtering: strict empty-membership, widen/narrow without session reset, and the non-RTC-peer full-table rule), M77 (GR/LLGR stale preservation for the VPN and RTC families — RTC membership survives a PE restart, so no VPN blackout at re-establish), and the VPN scale receipt (Scenario E): 100k VPNv4 to 1,000 clients in 12.6 s uniform / 3.9 s with heterogeneous ~10 % RT memberships, and a single member's RT-membership flip at 100k staged routes hitting the wire in ~15 ms with zero policy evaluations. Config shape derived from tests/interop/configs/rustbgpd-m75-rtc-rr.toml and rustbgpd-m77-gr-rr.toml.

Config

[global]
asn = 65000
router_id = "10.0.0.1"
listen_port = 179
cluster_id = "10.0.0.1"
# RFC 8212 posture, stated explicitly. It governs eBGP sessions only, so
# these iBGP clients are unaffected; an eBGP neighbor added later needs
# explicit import and export policy.
ebgp_requires_policy = true

[global.telemetry]
prometheus_addr = "127.0.0.1:9179"
log_format = "json"

# Owner-only local socket (default mode 0600): clients are authorized as the
# implicit "local-operator" principal — no [security.grpc] block needed.
[global.telemetry.grpc_uds]
path = "/var/lib/rustbgpd/grpc.sock"

# PE clients: VPN families plus "rtc" (RFC 4684, AFI 1 / SAFI 132).
# Each PE advertises its RT membership as RTC NLRIs; the RR filters
# its VPN reflection per peer accordingly. Semantics are strict: an
# RTC-negotiated peer with an empty advertised membership receives
# NO VPN routes (fail closed) until its first RTC announce arrives.
#
# GR/LLGR on the VPN + RTC families keeps both the VPN routes AND the
# peer's RTC membership across a PE restart — without membership
# retention, a restarting PE would blackhole its VPNs until it
# re-advertised every RT (M77 proves the retention).
[peer_groups.pe-clients]
hold_time = 90
families = ["l3vpn_ipv4_unicast", "l3vpn_ipv6_unicast", "rtc"]
route_reflector_client = true
graceful_restart = true
gr_stale_routes_time = 120
llgr_stale_time = 300

[[neighbors]]
address = "10.0.0.11"
remote_asn = 65000
description = "pe-1"
peer_group = "pe-clients"

[[neighbors]]
address = "10.0.0.12"
remote_asn = 65000
description = "pe-2"
peer_group = "pe-clients"

# A consumer WITHOUT "rtc" on purpose: a peer that has not negotiated
# SAFI 132 must receive the full unfiltered VPN table (RFC 4684 rule;
# M75 pins it). This is the controller/analytics-feed shape.
[[neighbors]]
address = "10.0.0.100"
remote_asn = 65000
description = "controller-feed"
hold_time = 90
route_reflector_client = true
families = ["l3vpn_ipv4_unicast", "l3vpn_ipv6_unicast"]

Verify

$ export RUSTBGPD_ADDR=unix:///var/lib/rustbgpd/grpc.sock
$ rbgp neighbor

All PEs Established. The per-peer detail view (rbgp neighbor 10.0.0.11) shows each PE's Update Group line: since ADR-0099, RTC negotiation is part of the group key, not a per-peer fallback — PEs with entirely different RT memberships still share one group and one staging pass; the RFC 4684 filter is applied per member at emit time.

The VPN table and the membership driving the filter:

$ rbgp rib vpn                          # VPNv4/VPNv6: RD, prefix, label, RTs
$ rbgp rib vpn -a vpnv6                 # VPNv6 only
$ rbgp rib rtc                          # RT-Constrain NLRIs per peer
$ rbgp rib rtc --neighbor 10.0.0.11     # what pe-1 says it imports

There is no per-peer VPN advertised-route listing: rbgp rib advertised does not accept a VPN family. Check what a PE gets one route at a time with the VPN export ladder, which includes the RT-membership gate:

$ rbgp rib --prefix 10.1.0.0/24 advertised 10.0.0.12 --explain --rd 65000:1

Toward an RTC peer the route passes rt_membership only when its RTs intersect that peer's membership; toward controller-feed, which did not negotiate RT-Constrain, the gate reports n/a and export is unfiltered. For the full outbound view, capture a BMP rib_out_post stream (monitoring feed).

Refresh a PE's VPN view without touching the session (the stale lifecycle is BoRR/EoRR-bounded per RFC 7313):

$ rbgp neighbor 10.0.0.11 softreset --family l3vpn_ipv4_unicast

Watch

Same dashboard rows as the unicast RR recipe — the update-group gauges (bgp_update_groups, bgp_update_group_members{group}, bgp_update_group_fallback_peers) cover the VPN groups too (ADR-0099), and bgp_session_state_transitions_total catches PE flaps. RIB scale panels in the Grafana overview track table growth as VPNs are provisioned.

Failure modes

A PE gets no VPN routes although its session is Established. First suspect: empty RTC membership — strict fail-closed is the designed behavior, not a bug. Check what the PE has advertised:

$ rbgp rib rtc --neighbor 10.0.0.11

Empty output = the PE negotiated SAFI 132 but announced no RT membership; fix the PE's VRF import config (or its RTC default-route origination). Confirm with the VPN export ladder, which includes the RT-membership gate:

$ rbgp rib --prefix 10.1.0.0/24 advertised 10.0.0.11 --explain --rd 65000:1

A STOP at rt_membership names exactly this condition. The VPN ladder runs family → best_route → llgr → rt_membership → split_horizon → rr_reflection → export_policy → adj_rib_out; a well-known-community suppression stops before export_policy.

The controller feed receives "too much". By design: no SAFI 132 negotiated → full table. If the consumer should be filtered, negotiate rtc on that peer and advertise membership from its side.

Stale VPN routes after a PE restart. Bounded by the GR window then llgr_stale_time, same as unicast — and deliberately so: the RTC membership is preserved through the same mechanism, so reflection resumes without a blackout when the PE returns (M77). If a PE never returns, both the routes and its membership expire with LLGR.

A VRF is torn down on a PE but its routes linger on other PEs. Check what the RR still holds from that PE with rbgp rib vpn --neighbor <pe> (-a vpnv4 or -a vpnv6 to narrow). VPN routes have no route-event history, so confirm the withdraw arrived with a BMP rib_in_pre collector or the PE's logs.

Source on GitHub

On this page