Skip to content

🧪 Lab 03 · Route reflectors

Validated on Arista cEOS 4.32.0F, 2026-08-03. All output captured live.

Time: ~45 minutes · Nodes: 3 (the same topology as Labs 01 and 02)

Build an iBGP network that silently fails to distribute routes, understand exactly why, then fix it with one command per neighbour.


What you'll learn

  • Why iBGP never re-advertises to another iBGP peer, and what that costs
  • How a route reflector breaks that rule safely
  • ORIGINATOR_ID and CLUSTER_LIST — loop prevention without the AS path
  • Why route reflection is how every modern fabric is built

Topology

The same three nodes as Lab 01, wired identically — but this time all three are in one AS, and r1's position in the middle makes it the natural hub.

graph LR
    R2["r2 · 2.2.2.2<br/>172.16.20.0/24<br/><i>client</i>"] ---|iBGP| R1["r1 · 1.1.1.1<br/><i>route reflector</i>"]
    R1 ---|iBGP| R3["r3 · 3.3.3.3<br/>172.16.30.0/24<br/><i>client</i>"]

    classDef rr fill:#2e7d32,stroke:#a5d6a7,color:#ffffff,stroke-width:2px,font-size:14px;
    classDef cl fill:#1565c0,stroke:#90caf9,color:#ffffff,stroke-width:2px,font-size:14px;
    class R1 rr; class R2,R3 cl;

Quick Start — Standalone Execution Guide (Location: labs/bgp-lab/)

Step 1 · Deploy the Lab Fabric (if not already running)

cd labs/bgp-lab
sudo containerlab deploy -t topology.clab.yml --max-workers 1

Step 2 · Launch Standalone Lab 03 Walkthrough

./run.sh --lab03

Alternative Execution Options (Manual CLI)
  • Manual Line-by-Line CLI Execution: Interactive CLI shell on any container node:
    docker exec -it clab-bgp-lab-r1 Cli
    
    Or push individual step snippets using stdin: docker exec -i clab-bgp-lab-r1 Cli -p 15 < steps/lab03-r1-reflector.cfg
Device Role Loopback Advertises
r1 route reflector 1.1.1.1
r2 client 2.2.2.2 172.16.20.0/24
r3 client 3.3.3.3 172.16.30.0/24

Everything is AS 65001. Each client peers only with r1 — there is no client-to-client session, which is the whole point.

Same fabric, different design

Labs 01, 02 and 03 all run on labs/bgp-lab. Lab 01 splits it across two ASes to teach eBGP and iBGP; Lab 02 swaps its IGP; this lab puts everything in one AS to teach reflection.

Redeploy for a clean start rather than un-picking Lab 01's config:

cd labs/bgp-lab
sudo containerlab destroy -t topology.clab.yml
sudo containerlab deploy -t topology.clab.yml --max-workers 1

Note Ethernet2 is in OSPF here. In Lab 01 it faced another AS and was deliberately excluded; now it's an internal link like any other.


Step 1 · Deploy

topology.clab.yml
name: bgp-lab

topology:
  nodes:
    r1: { kind: arista_ceos, image: ceos:4.32.0F }
    r2: { kind: arista_ceos, image: ceos:4.32.0F }
    r3: { kind: arista_ceos, image: ceos:4.32.0F }

  # endpoints MUST be lowercase ethN — cEOS entrypoint counts eth* interfaces
  links:
    - endpoints: ["r1:eth1", "r2:eth1"]   # 10.0.12.0/24  internal AS 65001
    - endpoints: ["r1:eth2", "r3:eth1"]   # 10.0.13.0/24  eBGP to AS 65002
sudo containerlab deploy -t topology.clab.yml --max-workers 1

Verify:

./run.sh 01
  r1   2 ready, 0 unknown
  r2   1 ready, 0 unknown
  r3   1 ready, 0 unknown
  ✅ DONE

DONE when every port reports a real type, not Unknown.


Step 2 · Underlay and plain iBGP

Everything is one AS, so both links go in OSPF and every router peers with r1. Configure this exactly as written — it is deliberately incomplete.

! Lab 03 — r1 becomes the hub. Note BOTH links are now in OSPF: in Lab 01
! Ethernet2 faced another AS and was deliberately excluded. Here everything
! is AS 65001, so the whole topology is one IGP domain.
configure
ip routing
service routing protocols model multi-agent
!
interface Loopback0
 ip address 1.1.1.1/32
 ip ospf area 0.0.0.0
!
interface Ethernet1
 no switchport
 ip address 10.0.12.1/24
 ip ospf area 0.0.0.0
 ip ospf network point-to-point
!
interface Ethernet2
 no switchport
 ip address 10.0.13.1/24
 ip ospf area 0.0.0.0
 ip ospf network point-to-point
!
router ospf 1
 router-id 1.1.1.1
!
router bgp 65001
 router-id 1.1.1.1
 no bgp default ipv4-unicast
 neighbor 2.2.2.2 remote-as 65001
 neighbor 2.2.2.2 update-source Loopback0
 neighbor 3.3.3.3 remote-as 65001
 neighbor 3.3.3.3 update-source Loopback0
 address-family ipv4
  neighbor 2.2.2.2 activate
  neighbor 3.3.3.3 activate
!
end
configure
ip routing
service routing protocols model multi-agent
!
interface Loopback0
 ip address 2.2.2.2/32
 ip ospf area 0.0.0.0
!
interface Loopback100
 ip address 172.16.20.1/24
!
interface Ethernet1
 no switchport
 ip address 10.0.12.2/24
 ip ospf area 0.0.0.0
 ip ospf network point-to-point
!
router ospf 1
 router-id 2.2.2.2
!
router bgp 65001
 router-id 2.2.2.2
 no bgp default ipv4-unicast
 neighbor 1.1.1.1 remote-as 65001
 neighbor 1.1.1.1 update-source Loopback0
 address-family ipv4
  neighbor 1.1.1.1 activate
  network 172.16.20.0/24
!
end
configure
ip routing
service routing protocols model multi-agent
!
interface Loopback0
 ip address 3.3.3.3/32
 ip ospf area 0.0.0.0
!
interface Loopback100
 ip address 172.16.30.1/24
!
interface Ethernet1
 no switchport
 ip address 10.0.13.3/24
 ip ospf area 0.0.0.0
 ip ospf network point-to-point
!
router ospf 1
 router-id 3.3.3.3
!
router bgp 65001
 router-id 3.3.3.3
 no bgp default ipv4-unicast
 neighbor 1.1.1.1 remote-as 65001
 neighbor 1.1.1.1 update-source Loopback0
 address-family ipv4
  neighbor 1.1.1.1 activate
  network 172.16.30.0/24
!
end

Verify the underlay:

docker exec clab-bgp-lab-r1 Cli -p 15 -c "show ip ospf neighbor"
Neighbor ID     Instance VRF      Pri State    Dead Time   Address      Interface
3.3.3.3         1        default  0   FULL     00:00:35    10.0.13.3    Ethernet2
2.2.2.2         1        default  0   FULL     00:00:35    10.0.12.2    Ethernet1

Verify the sessions:

docker exec clab-bgp-lab-r1 Cli -p 15 -c "show ip bgp summary" | tail -3
  Neighbor V AS           MsgRcvd   MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc
  2.2.2.2  4 65001              5         4    0    0 00:00:13 Estab   1      1
  3.3.3.3  4 65001              5         4    0    0 00:00:12 Estab   1      1

Both Estab, one prefix received from each. Everything looks correct.

DONE when OSPF is FULL on both links and both BGP peers are Estab.


Step 3 · Find the silent failure

The hub has everything:

docker exec clab-bgp-lab-r1 Cli -p 15 -c "show ip bgp" | tail -3
          Network                Next Hop        Metric  LocPref Weight  Path
 * >      172.16.20.0/24         2.2.2.2         0       100     0       i
 * >      172.16.30.0/24         3.3.3.3         0       100     0       i

Both prefixes, both valid and best. Now ask a client:

docker exec clab-bgp-lab-r2 Cli -p 15 -c "show ip bgp" | tail -2
          Network                Next Hop        Metric  LocPref Weight  Path
 * >      172.16.20.0/24         -               -       -       0       i

One prefix — its own. r2 has no idea r3 exists.

Nothing is broken, and nothing works

Every session is Established. Every prefix was received. No errors, no logs, no failed check anywhere. And the network does not distribute routes.

This is the iBGP rule working exactly as specified: a route learned from an iBGP peer is never re-advertised to another iBGP peer. r1 learned both prefixes from iBGP peers, so it passes neither on.

The rule exists because iBGP doesn't prepend the AS path and so can't detect loops that way. Without it, a route could circulate indefinitely.

The textbook fix is a full mesh — every router peering with every other. Three routers is 3 sessions; fifty is 1,225, and every new router means touching every existing one. It doesn't scale.


Step 4 · Reflect

One line per neighbour, on the hub only:

! The ONLY change needed to fix route distribution. Clients are not touched —
! they are ordinary iBGP speakers and never learn they are clients.
configure
router bgp 65001
 address-family ipv4
  neighbor 2.2.2.2 route-reflector-client
  neighbor 3.3.3.3 route-reflector-client
!
end

The clients are never reconfigured. They don't know they're clients — they're ordinary iBGP speakers. That's what makes route reflection deployable on a live network, and why it beat confederations.

Verify:

docker exec clab-bgp-lab-r2 Cli -p 15 -c "show ip bgp" | tail -3
          Network            Next Hop    Metric  LocPref Weight  Path
 * >      172.16.20.0/24     -           -       -       0       i
 * >      172.16.30.0/24     3.3.3.3     0       100     0       i Or-ID: 3.3.3.3 C-LST: 1.1.1.1

r3's prefix has arrived, carrying two new attributes. The view from r3 is the mirror image:

 * >      172.16.20.0/24     2.2.2.2     0       100     0       i Or-ID: 2.2.2.2 C-LST: 1.1.1.1
 * >      172.16.30.0/24     -           -       -       0       i

DONE when each client sees both prefixes.


Step 5 · The loop-prevention attributes

docker exec clab-bgp-lab-r2 Cli -p 15 -c "show ip bgp 172.16.30.0/24"
BGP routing table entry for 172.16.30.0/24
 Paths: 1 available
  Local
    3.3.3.3 from 1.1.1.1 (1.1.1.1)
      Origin IGP, metric 0, localpref 100, IGP metric 30, weight 0, tag 0
      Received 00:00:17 ago, valid, internal, best
      Originator: 3.3.3.3, Cluster list: 1.1.1.1

Read the key line carefully:

3.3.3.3 from 1.1.1.1 (1.1.1.1) — the next hop is r3, but the route was received from r1. The reflector passed it on without inserting itself into the data path. Traffic goes to r3's address; only the advertisement went via r1.

Attribute Value Job
Originator 3.3.3.3 router ID of the original advertiser. A router seeing its own ID here discards the route — so r3 won't re-accept its own prefix.
Cluster list 1.1.1.1 reflectors traversed. A reflector seeing its own cluster ID discards it — preventing loops between reflectors.

Together these replace the AS-path loop detection iBGP doesn't have. Both are optional non-transitive, so they exist only inside the AS and never leak out via eBGP.


Step 6 · Prove it forwards

docker exec clab-bgp-lab-r2 Cli -p 15 -c "ping 172.16.30.1 source 172.16.20.1 repeat 3"
3 packets transmitted, 3 received, 0% packet loss

Each client still has exactly one BGP session:

docker exec clab-bgp-lab-r2 Cli -p 15 -c "show ip bgp summary" | grep -c Estab
1
Routers Full mesh With one RR
3 3 2
10 45 9
50 1,225 49
100 4,950 99

DONE.

Control plane and data plane are separate concerns

Here the reflector also sits in the data path, because the topology is a chain with r1 in the middle. That's incidental.

A route reflector does not have to carry traffic. It's a control-plane function, and in large networks reflectors are often dedicated devices — or virtual machines — off the forwarding path entirely. The from 1.1.1.1 versus next-hop 3.3.3.3 distinction above is what makes that possible.


Break & observe

Remove client status from r2 and watch reflection stop:

docker exec -i clab-bgp-lab-r1 Cli -p 15 <<'EOF'
configure
router bgp 65001
 address-family ipv4
  no neighbor 2.2.2.2 route-reflector-client
end
EOF

r2 loses 172.16.30.0/24 — it's an ordinary iBGP peer again, so r1 won't reflect to it. r3 also loses 172.16.20.0/24, because a route from a non-client is only reflected to clients.

That second effect is the reflection rule table made concrete:

Learned from Reflected to
Client other clients and non-clients
Non-client clients only
eBGP everyone

Restore:

docker exec -i clab-bgp-lab-r1 Cli -p 15 <<'EOF'
configure
router bgp 65001
 address-family ipv4
  neighbor 2.2.2.2 route-reflector-client
end
EOF

Production considerations

One reflector is a single point of failure — for the control plane. Existing routes keep forwarding if it dies, but no new information propagates. Deploy two.

With two, choose cluster IDs deliberately:

  • Same cluster ID — reflectors ignore each other's reflected routes. Lower memory, but clients may lose paths if one session drops.
  • Different cluster IDs — each treats the other's routes as new. Better redundancy and path diversity, more memory and duplicate updates. The more common modern choice.

Reflection costs path diversity. The reflector advertises only its own best path, so clients see one path chosen from the reflector's IGP position rather than their own — which can be sub-optimal. add-path lets it advertise several.


Troubleshooting

Symptom Cause
Clients see only their own prefix route-reflector-client missing — this lab's step 3
Some clients see routes, others don't client configured on only some neighbours
Route present but unusable next hop unreachable — check the IGP
Routes loop or churn cluster IDs misconfigured between multiple reflectors
Client sees fewer paths than expected expected — reflectors advertise only best path

Interview questions

Every iBGP session is Established but clients only see their own routes. Why?

A route learned from an iBGP peer is never re-advertised to another iBGP peer. The reflector received all prefixes from iBGP peers, so it passes none on. The fix is either a full mesh or marking the neighbours as route-reflector clients.

How does route reflection prevent loops without the AS path?

Two optional non-transitive attributes. ORIGINATOR_ID carries the original advertiser's router ID — a router seeing its own discards the route. CLUSTER_LIST records reflectors traversed — a reflector seeing its own cluster ID discards it. Both stay inside the AS.

Which routers need reconfiguring to deploy a route reflector?

Only the reflector. Clients are ordinary iBGP speakers and don't know they're clients — which is precisely why reflection can be introduced incrementally on a live network, and why it won out over confederations.

Does a route reflector have to be in the data path?

No. It's a control-plane function. Route reflection changes which routes are advertised, not where traffic goes — the next hop still points at the originating router. Reflectors are often dedicated devices or VMs off the forwarding path.

Two reflectors — same or different cluster ID?

Same means they ignore each other's reflected routes: less memory, fewer paths per client. Different means each treats the other's as new: better redundancy and diversity, more memory. Different is the more common modern choice, since memory is cheaper than an outage.

What do you lose by using route reflection?

Path diversity. A reflector advertises only its own best path, chosen from its own IGP position, so clients see fewer options than a full mesh would give and may route sub-optimally. add-path mitigates it at the cost of memory and update volume.


Where you've seen this before

This is not a niche technique — it's how modern fabrics are built:

  • Phase 4 · EVPN uses spines as route reflectors for the iBGP-EVPN overlay. Leaves peer only with spines. Re-read that overlay config now — it's this lab with a different address family.
  • Phase 3 · MPLS L3VPN reflects VPNv4 routes between PEs identically.

🧠 Google Network Infra Knowledge Sharing

[!NOTE]

Production Deep Dive & Hyperscale Architecture

  1. iBGP Scaling Math & Full-Mesh Limits:
  2. A full mesh requires \frac{N(N-1)}{2} TCP sessions. At Google scale (1,000+ switches in a single cluster fabric), a full mesh requires ~500,000 BGP sessions, which would exhaust memory and CPU resources.
  3. Route Reflectors reduce session count to 2 \times N (dual redundant RRs per cluster), reducing BGP control plane overhead by 99%+.

  4. Loop Prevention: ORIGINATOR_ID & CLUSTER_LIST:

  5. Since AS_PATH is not modified across iBGP sessions, Route Reflectors introduce two optional non-transitive attributes:

    • ORIGINATOR_ID: Set to the Router ID of the originating iBGP speaker. If a router receives a route with its own ORIGINATOR_ID, it drops the update.
    • CLUSTER_LIST: Sequence of Cluster IDs traversed. If an RR receives a route containing its own Cluster ID, it drops the update.
  6. Leaf-Spine Fabrics (cEOS EVPN / IP Core):

  7. Spines act as Control-Plane Route Reflectors for all Leaf VTEPs.
  8. Leaf switches only peer with the Spine RRs, eliminating the need for Leaf-to-Leaf iBGP sessions. The Spines remain out of tenant VRF data-plane encapsulation while reflecting EVPN Type-2/Type-3/Type-5 routes.

Clean up

sudo containerlab destroy -t topology.clab.yml