🧪 Lab 01 · eBGP, iBGP and next-hop-self¶
✅ Validated on Arista cEOS 4.32.0F, 2026-08-03. Every output below was captured from this fabric — nothing is written from memory.
Time: ~45 minutes · Nodes: 3
[!NOTE] Sequential Link to Lab 02: Lab 02 (IS-IS Underlay Migration) builds directly on top of the working fabric you configure in this lab. Keep the fabric running after completing Lab 01 to transition seamlessly into Lab 02!
Quick Start — Step-by-Step 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 the Fully Guided Interactive Walkthrough
./run.sh --guided
Alternative Execution Options (Automated Push or Manual CLI)
- Fast Automated Script Push:
./run.sh 02 # apply + verify step 02 automatically ./run.sh --all # run all steps in order - Manual Line-by-Line CLI Execution:
Interactive CLI shell on any container node:
Or push individual step snippets via stdin:
docker exec -it clab-bgp-lab-r1 Clidocker exec -i clab-bgp-lab-r1 Cli -p 15 < steps/02-r1-underlay.cfg
Build two autonomous systems, peer them, and hit the single most common iBGP mistake on purpose — then fix it and prove traffic flows.
What you'll learn¶
- Why eBGP and iBGP are the same protocol with different rules
- Why iBGP peers over loopbacks and eBGP doesn't
- The next-hop problem that breaks almost every first iBGP deployment
- How to read
show ip bgpand tell a valid route from a usable one
Topology¶
graph LR
subgraph AS65001["AS 65001"]
R2["r2<br/>2.2.2.2<br/>172.16.20.0/24"] ---|"10.0.12.0/24<br/>OSPF + iBGP"| R1["r1<br/>1.1.1.1"]
end
subgraph AS65002["AS 65002"]
R3["r3<br/>3.3.3.3<br/>172.16.30.0/24"]
end
R1 ---|"10.0.13.0/24<br/>eBGP"| R3
classDef n fill:#1565c0,stroke:#90caf9,color:#ffffff,stroke-width:2px,font-size:14px;
classDef e fill:#2e7d32,stroke:#a5d6a7,color:#ffffff,stroke-width:2px,font-size:14px;
class R1,R2 n; class R3 e;
| Device | AS | Loopback0 | Advertises |
|---|---|---|---|
| r1 | 65001 | 1.1.1.1/32 | — (border router) |
| r2 | 65001 | 2.2.2.2/32 | 172.16.20.0/24 |
| r3 | 65002 | 3.3.3.3/32 | 172.16.30.0/24 |
Step 1 · Deploy¶
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
--max-workers 1 serialises startup. Booting concurrently under Rosetta can race
the wiring and leave interfaces as type Unknown.
Verify — every data-plane port must show a real type:
for n in r1 r2 r3; do docker exec clab-bgp-lab-$n Cli -p 15 -c "show interfaces status" | grep -E "^Et[12]"; done
Et1 connected 1 full 1G EbraTestPhyPort
Et2 connected 1 full 1G EbraTestPhyPort
Et1 connected 1 full 1G EbraTestPhyPort
Et1 connected 1 full 1G EbraTestPhyPort
✅ DONE when every interface reads EbraTestPhyPort. If any says Unknown,
destroy and redeploy — do not docker restart, which destroys the veth pairs.
Step 2 · Underlay inside AS 65001¶
iBGP peers over loopbacks, so the IGP must make those loopbacks reachable first. This is the dependency that catches people: BGP looks broken when the real fault is underneath it.
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
!
router ospf 1
router-id 1.1.1.1
!
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 Ethernet1
no switchport
ip address 10.0.12.2/24
ip ospf area 0.0.0.0
ip ospf network point-to-point
!
interface Loopback100
ip address 172.16.20.1/24
!
router ospf 1
router-id 2.2.2.2
!
end
Apply with a heredoc — -i is mandatory:
docker exec -i clab-bgp-lab-r1 Cli -p 15 <<'EOF'
configure
...
end
EOF
Two things you'll see, only one is a problem
Benign: while applying, EOS may print
IP configuration will be ignored while interface Ethernet1 is not a routed port.
That's emitted mid-parse before no switchport takes effect. Check the result
with show running-config interfaces Ethernet1 — if the address is there, it
applied.
Not benign: a heredoc that returns instantly with no output at all.
That means -i was missing, stdin was never attached, and nothing was
configured. Cli exits 0, so it looks like success.
Notice Ethernet2 has no ip ospf area. That's deliberate — it faces another
AS and must not be in your IGP. It matters in step 4.
Verify:
docker exec clab-bgp-lab-r1 Cli -p 15 -c "show ip ospf neighbor"
Neighbor ID Instance VRF Pri State Dead Time Address Interface
2.2.2.2 1 default 0 FULL 00:00:33 10.0.12.2 Ethernet1
✅ DONE when the neighbour is FULL. Anything else — stop and fix it here.
Step 3 · The BGP sessions¶
Two sessions, configured differently for reasons worth understanding.
configure
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 10.0.13.3 remote-as 65002
address-family ipv4
neighbor 2.2.2.2 activate
neighbor 10.0.13.3 activate
!
end
configure
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
!
interface Ethernet1
no switchport
ip address 10.0.13.3/24
!
interface Loopback100
ip address 172.16.30.1/24
!
router bgp 65002
router-id 3.3.3.3
no bgp default ipv4-unicast
neighbor 10.0.13.1 remote-as 65001
address-family ipv4
neighbor 10.0.13.1 activate
network 172.16.30.0/24
!
end
Why iBGP uses loopbacks and eBGP uses interface addresses:
iBGP peers are usually multiple hops apart with several possible paths between them. Peering from a loopback means the session survives any single link failing — the IGP just reroutes. That only works if the IGP advertises the loopbacks, which is step 2.
eBGP peers are typically directly connected on a link with no IGP between them. There is no alternate path, so the interface address is the natural choice — and eBGP defaults to TTL 1, which assumes exactly that.
no bgp default ipv4-unicast
Without it, every neighbour is auto-activated for IPv4 the moment you define it. Being explicit is the modern habit and essential once you add address families like EVPN — you rarely want every peer in every family.
Verify:
docker exec clab-bgp-lab-r1 Cli -p 15 -c "show ip bgp summary"
BGP summary information for VRF default
Router identifier 1.1.1.1, local AS number 65001
Neighbor V AS MsgRcvd MsgSent InQ OutQ Up/Down State PfxRcd PfxAcc
2.2.2.2 4 65001 5 7 0 0 00:00:07 Estab 1 1
10.0.13.3 4 65002 5 5 0 0 00:00:26 Estab 1 1
✅ DONE when both peers show Estab and PfxRcd 1.
If a session sits in Active or Idle, BGP can't reach the peer. For the iBGP one
that almost always means the loopback isn't in OSPF.
Step 4 · Break it — the next-hop problem¶
Everything says Established. Look at r2's table:
docker exec clab-bgp-lab-r2 Cli -p 15 -c "show ip bgp"
Network Next Hop Metric AIGP LocPref Weight Path
* > 172.16.20.0/24 - - - - 0 i
* > 172.16.30.0/24 10.0.13.3 0 - 100 0 65002 i
* > — valid and best. Looks perfect.
It isn't. The next hop is 10.0.13.3, an address on the eBGP link. Ask whether
r2 can actually reach it:
docker exec clab-bgp-lab-r2 Cli -p 15 -c "show ip route 10.0.13.3"
Gateway of last resort:
S 0.0.0.0/0 [1/0]
via 172.20.20.1, Management0
No specific route. Only the default. Because Ethernet2 was deliberately left
out of OSPF in step 2, nothing inside AS 65001 knows how to reach 10.0.13.0/24.
Now the part that makes this genuinely nasty:
docker exec clab-bgp-lab-r2 Cli -p 15 -c "show ip route 172.16.30.0/24"
B I 172.16.30.0/24 [200/0]
via 172.20.20.1, Management0
The route installed — pointing out the management interface. BGP resolved the unreachable next hop against the default route, and the default route is management.
This is worse than a visible failure
Every check says healthy. The session is Established, the prefix is received,
the route is * > valid, and it's installed in the FIB. Nothing is red.
But data traffic is being handed to the out-of-band management network. In production that's either a black hole or, worse, a path that works just well enough to hide the fault for months.
In a lab, show ip bgp showing a valid route is not proof of anything.
Always confirm the next hop is reachable via a data-plane route.
Why does this happen at all? When an eBGP router advertises a prefix, the next hop is its own interface address. When r1 passes that to r2 over iBGP, it keeps the next hop unchanged — that's the rule. r2 receives a next hop belonging to a link in a different AS, which it has no route to.
Step 5 · Fix it¶
Tell r1 to overwrite the next hop with its own address when advertising to iBGP peers:
configure
router bgp 65001
address-family ipv4
neighbor 2.2.2.2 next-hop-self
!
end
Or run the step: ./run.sh 05
Verify:
docker exec clab-bgp-lab-r2 Cli -p 15 -c "show ip bgp"
Network Next Hop Metric AIGP LocPref Weight Path
* > 172.16.20.0/24 - - - - 0 i
* > 172.16.30.0/24 1.1.1.1 0 - 100 0 65002 i
Next hop is now 1.1.1.1 — r1's loopback, which OSPF advertises.
docker exec clab-bgp-lab-r2 Cli -p 15 -c "show ip route 172.16.30.0/24"
B I 172.16.30.0/24 [200/0]
via 10.0.12.1, Ethernet1
via 10.0.12.1, Ethernet1 — the real data path, not management.
✅ DONE when the route resolves via Ethernet1. Compare against step 4: the BGP
table looked almost identical, but the FIB entry changed completely.
Step 6 · Prove it forwards¶
Control plane agreement is not forwarding. Test it:
docker exec clab-bgp-lab-r2 Cli -p 15 -c "ping 172.16.30.1 source 172.16.20.1 repeat 3"
PING 172.16.30.1 (172.16.30.1) from 172.16.20.1 : 72(100) bytes of data.
80 bytes from 172.16.30.1: icmp_seq=1 ttl=63 time=22.3 ms
80 bytes from 172.16.30.1: icmp_seq=2 ttl=63 time=13.2 ms
80 bytes from 172.16.30.1: icmp_seq=3 ttl=63 time=2.42 ms
--- 172.16.30.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 22ms
docker exec clab-bgp-lab-r2 Cli -p 15 -c "traceroute 172.16.30.1 source 172.16.20.1"
traceroute to 172.16.30.1 (172.16.30.1), 30 hops max, 60 byte packets
1 10.0.12.1 (10.0.12.1) 0.247 ms 0.054 ms 0.020 ms
2 172.16.30.1 (172.16.30.1) 4.247 ms 4.636 ms 4.752 ms
r2 → r1 → r3, across an AS boundary. ✅ DONE.
Step 7 · Read the AS path¶
The same prefixes look different depending on where you stand:
Network Next Hop LocPref Weight Path
* > 172.16.20.0/24 10.0.13.1 100 0 65001 i
* > 172.16.30.0/24 - - 0 i
65001 in the path — learned across one AS boundary. Its own prefix has an
empty path.
Network Next Hop LocPref Weight Path
* > 172.16.20.0/24 2.2.2.2 100 0 i
* > 172.16.30.0/24 10.0.13.3 100 0 65002 i
172.16.20.0/24 has an empty path — learned via iBGP, and iBGP doesn't
prepend. The AS path only grows crossing an AS boundary.
That's also the loop-prevention mechanism: a router rejects any route whose AS path already contains its own AS number.
Why iBGP needs a full mesh
iBGP doesn't prepend the AS path, so it can't detect loops the way eBGP does. The protocol compensates with a hard rule: a route learned via iBGP is never re-advertised to another iBGP peer.
Loop-free, but it means every iBGP speaker must peer with every other — a full mesh, growing as n(n−1)/2. Route reflectors exist to break that, and they're Lab 02.
Platform note: administrative distance¶
Textbooks give eBGP an AD of 20 and iBGP 200. On this platform, measured:
r1: B E 172.16.30.0/24 [200/0] ← eBGP-learned
r2: B I 172.16.30.0/24 [200/0] ← iBGP-learned
Both 200, with no distance configured. The 20/200 split is Cisco IOS behaviour, not a BGP standard — Arista EOS defaults both to 200.
In an interview the expected answer is usually 20/200, but knowing it's vendor-specific is the better answer. Verify on the platform in front of you rather than assuming.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
Session stuck Idle/Active |
peer address unreachable | iBGP: loopback missing from OSPF. eBGP: check the interface address |
| iBGP up, no prefixes | peer not activated in the address family | neighbor X activate |
| Prefix received, route unusable | next hop unreachable | next-hop-self on the border router |
Route resolves via Management0 |
next hop matched the default route | same fix — check the FIB, not just show ip bgp |
network statement ignored |
no matching route in the RIB | the prefix must exist locally first |
| Heredoc silently does nothing | missing -i |
docker exec -i |
Interface type Unknown |
boot race | destroy + redeploy with --max-workers 1 |
Interview questions¶
Why does iBGP peer over loopbacks while eBGP uses interface addresses?
iBGP peers are usually several hops apart with redundant paths between them; a loopback session survives any single link failure because the IGP reroutes around it. eBGP peers are typically directly connected with no alternate path and no IGP between the ASes, so the interface address is natural — and eBGP's default TTL of 1 assumes exactly that.
What is next-hop-self and why is it needed?
When a router advertises an eBGP-learned prefix to an iBGP peer, it leaves the
next hop unchanged — pointing at an address in the neighbouring AS that internal
routers have no route to. next-hop-self rewrites it to the advertising
router's own address, which the IGP does advertise.
BGP shows a route as valid and best, but traffic doesn't reach it. What do you check?
Whether the next hop is reachable by a data-plane route. BGP marks a route
valid if the next hop resolves at all — including via a default route. In a
lab that default is often the management interface, so the route installs and
silently sends traffic out-of-band. Check show ip route <next-hop> and confirm
the FIB entry points at a real data interface.
Why must iBGP be fully meshed?
iBGP doesn't prepend the AS path, so it can't use path-based loop detection. The protocol compensates with the rule that a route learned from an iBGP peer is never re-advertised to another iBGP peer — which means every speaker must hear it directly. Route reflectors relax this.
A prefix in the routing table isn't advertised despite a network statement. Why?
The network statement only advertises a prefix that already exists in the RIB
with an exact match. If nothing local generates that exact prefix — no
interface, no static, no IGP route — BGP has nothing to advertise. Mask
mismatches are the usual culprit.
What's the administrative distance of eBGP and iBGP?
Traditionally 20 and 200 — but that's Cisco IOS, not a standard. Arista EOS defaults both to 200, verified on this lab. The better answer names the conventional values and notes they're vendor-specific.
🧠 Google Network Infra Knowledge Sharing¶
[!NOTE]
Production Deep Dive & Hyperscale Architecture¶
- BGP Recursive Next-Hop Lookup Mechanics:
- In hyperscale networks (Google B4/Jupiter), BGP routes are decoupled from physical interface topology. When a border router receives an eBGP prefix, it preserves the peer's next-hop IP.
- Internal routers perform a recursive table lookup: RIB checks BGP Next-Hop → RIB checks IGP table for Next-Hop reachability → FIB programs the hardware forwarding ASIC.
Without
next-hop-self(or explicit IGP propagation), the recursive lookup defaults to 0.0.0.0/0 (management/out-of-band interface) or fails completely, resulting in silent packet drops.Loopback Peering & Path Resilience:
- Hyperscale fabrics use
Loopback0interfaces for iBGP peering because loopbacks are independent of physical link status.If link
eth1fails, IGP (OSPF/IS-IS) instantly updates the path toLoopback0via an alternate ECMP link. The iBGP TCP session remains Established without dropping a single BGP prefix state.
no bgp default ipv4-unicastProduction Hygiene:- Google production fabrics run multi-family BGP (IPv4 Unicast, IPv6 Unicast, EVPN, VPNv4). Defaulting IPv4 unicast session activation causes unexpected route leaking when adding new BGP neighbors. Disabling default activation forces explicit policy declaration per address family.
Clean up¶
sudo containerlab destroy -t topology.clab.yml
Next: Lab 02 — route reflectors, breaking the iBGP full-mesh requirement. (In progress.)