This example demonstrates how to run one node as a pure BGP route reflector (RFC 4456) that distributes EVPN routes between the other nodes, removing the need for a full iBGP mesh or for the Top-of-Rack fabric to reflect the routes.
Overview #
The control-plane node runs a router with no tunnel endpoint: it does not participate in the data plane and only reflects control-plane routes. The worker nodes run a regular data-plane underlay whose only BGP session is the internal (iBGP) one to the route reflector.
A layer 2 VNI is stretched between the worker nodes. Since the clients peer only with the reflector, the EVPN type-2/type-3 routes can only be learned through the reflection path, while the VXLAN traffic flows directly between the workers.
Example Setup #
The full example can be found in the project repository and can be deployed by running:
make docker-build demo-route-reflector
The example configures:
- An
Underlaynamedroute-reflectoron the control-plane node, accepting dynamic iBGP sessions over a listen range and reflecting bothipv4unicast(VTEP reachability) andevpnroutes - An
Underlaynamedclienton the worker nodes, peering only with the route reflector - A disconnected
L2VNIon the worker nodes, plus one pod per worker attached to it via a macvlan interface
For details about the configuration fields, see the Route Reflector configuration guide.
Configuration #
OpenPERouter Configuration #
The route reflector underlay is selected on the control-plane node. It has no tunnelEndpoint, and accepts dynamic neighbors from the cluster subnet via listenRange instead of enumerating each node:
apiVersion: network.openperouter.io/v1alpha1
kind: Underlay
metadata:
name: route-reflector
namespace: openperouter-system
spec:
asn: 64514
interfaces:
- type: NetworkDevice
networkDevice:
interfaceName: toswitch1
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: Exists
routeReflector:
clusterID: 192.0.2.1
neighbors:
- type: Internal
listenRange: 192.168.11.0/24
addressFamilies:
- type: ipv4unicast
properties:
- type: routeReflectorClient
- type: evpn
properties:
- type: routeReflectorClient
The client underlay is selected on the worker nodes. It is a normal data-plane underlay whose only neighbor is the route reflector, peered as an internal (iBGP) session:
apiVersion: network.openperouter.io/v1alpha1
kind: Underlay
metadata:
name: client
namespace: openperouter-system
spec:
asn: 64514
interfaces:
- type: NetworkDevice
networkDevice:
interfaceName: toswitch1
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
tunnelEndpoint:
cidrs:
- 100.65.0.0/24
neighbors:
- type: Internal
address: 192.168.11.3 # the route reflector on the leafkind1 switch subnet
Finally, a disconnected L2VNI (no VRF) is stretched between the client nodes. The node selector keeps it off the route reflector node, which has no tunnel endpoint:
apiVersion: network.openperouter.io/v1alpha1
kind: L2VNI
metadata:
name: reflected
namespace: openperouter-system
spec:
vni: 300
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
hostMaster:
type: LinuxBridge
linuxBridge:
lifecycle: Managed
Configuration Notes:
routeReflector: marks the control-plane router as a route reflector and sets the BGPcluster-idlistenRange: accepts dynamic iBGP sessions from any node in the cluster subnet, so nodes can be added without touching the reflector configurationrouteReflectorClient: reflects the routes received in that address family to the other clients;ipv4unicastcarries the VTEP/32reachability,evpncarries the type-2/type-3 routeshostMaster.lifecycle: Managed: instructs OpenPERouter to create a bridge local to the node that can be used to access the L2 domain
Network Attachment Definition #
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
name: macvlan-conf
spec:
config: '{
"cniVersion": "0.3.0",
"type": "macvlan",
"master": "br-hs-300",
"mode": "bridge",
"ipam": {
"type": "static"
}
}'
A network attachment definition creating a macvlan interface on top of the br-hs-300 bridge created by OpenPERouter for VNI 300. Both pods are on the same subnet, so no gateway is needed.
Workload Configuration #
Two pods, one per worker node, with a Multus secondary network attached to the L2VNI bridge:
apiVersion: v1
kind: Pod
metadata:
name: first
annotations:
k8s.v1.cni.cncf.io/networks: '[{
"name": "macvlan-conf",
"namespace": "default",
"ips": ["192.171.31.2/24"]
}]'
spec:
nodeSelector:
kubernetes.io/hostname: pe-kind-worker
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: agnhost
image: registry.k8s.io/e2e-test-images/agnhost:2.45
command: ["/agnhost", "netexec", "--http-port=8090"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
ports:
- containerPort: 8090
name: http
The second pod is identical, pinned to pe-kind-worker2 with IP 192.171.31.3/24.
Validation #
Route Reflector Sessions #
The router pods can be listed with their nodes to identify the reflector (control-plane) and the clients (workers):
kubectl get pods -n openperouter-system -l app=router -o wide
On the route reflector router pod, the BGP summary shows the sessions with the clients. Dynamic neighbors accepted via the listen range are prefixed with *:
kubectl exec -n openperouter-system <router-pod-on-control-plane> -c frr -- vtysh -c "show bgp summary"
Expected output:
L2VPN EVPN Summary:
BGP router identifier 10.0.0.0, local AS number 64514 VRF default
...
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt Desc
*192.168.11.4 4 64514 29 27 3 0 0 00:00:59 2 4 N/A
*192.168.11.5 4 64514 29 28 3 0 0 00:00:59 2 4 N/A
Total number of neighbors 2
* - dynamic neighbor
2 dynamic neighbor(s), limit 65535
The reflector’s generated FRR configuration can be inspected as well:
kubectl exec -n openperouter-system <router-pod-on-control-plane> -c frr -- vtysh -c "show running-config"
It contains the route reflection bits described in the configuration guide:
router bgp 64514
bgp cluster-id 192.0.2.1
bgp listen limit 65535
neighbor 192.168.11.0/24 peer-group
neighbor 192.168.11.0/24 remote-as internal
bgp listen range 192.168.11.0/24 peer-group 192.168.11.0/24
!
address-family ipv4 unicast
neighbor 192.168.11.0/24 activate
neighbor 192.168.11.0/24 route-reflector-client
exit-address-family
!
address-family l2vpn evpn
neighbor 192.168.11.0/24 activate
neighbor 192.168.11.0/24 route-reflector-client
exit-address-family
Reflected EVPN Routes #
On a worker’s router pod, the type-2 routes of the pod running on the other worker must have been learned through the reflector. Reflected routes carry the Originator and Cluster list attributes:
kubectl exec -n openperouter-system <router-pod-on-worker> -c frr -- vtysh -c "show bgp l2vpn evpn route type macip"
Expected output (trimmed):
Route Distinguisher: 10.0.0.2:2
*>i[2]:[0]:[48]:[aa:bb:cc:dd:ee:ff]:[32]:[192.171.31.3]
100.65.0.2 100 0 i
RT:64514:300 ET:8
The route detail shows it was reflected, with the next hop preserved (the originating worker’s VTEP) and the reflector’s cluster-id in the cluster list:
kubectl exec -n openperouter-system <router-pod-on-worker> -c frr -- vtysh -c "show bgp l2vpn evpn route rd 10.0.0.2:2"
Route [2]:[0]:[48]:[aa:bb:cc:dd:ee:ff]:[32]:[192.171.31.3] VNI 300
...
Origin IGP, localpref 100, valid, internal, best (First path received)
Originator: 10.0.0.2, Cluster list: 192.0.2.1
Pod-to-Pod Connectivity #
We can check that the first pod is able to curl the second, and the client IP is the one corresponding to the secondary interface:
kubectl exec -it first -- curl 192.171.31.3:8090/clientip
Expected output:
192.171.31.2:47420
Since the pods’ only common network is the reflected L2VNI, the traffic proves the type-2/type-3 routes were distributed by the route reflector: removing the routeReflector underlay (or the routeReflectorClient properties) breaks the connectivity between the pods.
Traffic can be monitored in the OpenPERouter’s namespace on one of the workers to validate that it goes directly worker-to-worker over the VXLAN overlay, without transiting the reflector:
21:12:33.201432 toswitch1 Out IP 100.65.0.1.40875 > 100.65.0.2.4789: VXLAN, flags [I] (0x08), vni 300
IP 192.171.31.2 > 192.171.31.3: ICMP echo request, id 4386, seq 0, length 64