Free lab kit · GNS3 or containerlab · FRRouting 10.5 · ~15 minutes

Build an AI-cluster fabric: BGP unnumbered, ECMP, and the 9-second failure BFD fixes

Five routers, zero IP addresses on the fabric links, every flow spread across every spine, and one silent cable fault that drops 70 to 80 packets — until one line of config turns it into about 4.

Bill of materials → build this fabric on real hardware

Download the free lab kit (.zip)

GNS3 portable project + one-command containerlab topology, all configs, the demo script and exercise answers. MIT licensed. How to run it ↓

  1. Why this matters
  2. The topology
  3. IP & ASN plan
  4. Config walkthrough
  5. Verification
  6. The demo: gray failure vs BFD
  7. Hands-on exercises
  8. Recap & try it yourself

By the end you can:

  1. Explain why AI and data-center fabrics run an L3 Clos with eBGP instead of an IGP or a big L2.
  2. Configure BGP unnumbered in FRR — peer by interface name, no per-link IPs — and explain RFC 8950.
  3. Read ECMP in the routing table and prove traffic actually spreads across spines.
  4. Explain why both spines share one ASN (RFC 7938).
  5. Tell a link going down from a link going silent, and use BFD to cut failover from ~9 s to ~300 ms.

1Why this matters

AI training is a team sport for GPUs

A training job splits work across thousands of GPUs that constantly swap gradients (an all-reduce). Every step waits for the slowest flow. One stalled path stalls the whole job — so the network needs huge, even bandwidth and very fast failover.

Clos: spine-leaf

Every leaf (top-of-rack switch) connects to every spine. Any host is exactly leaf → spine → leaf away, and there are as many equal paths as there are spines. Need more bandwidth? Add a spine. More racks? Add a leaf.

Why BGP inside the data center

RFC 7938: run eBGP on every link. One protocol, no flooding domain, each router is its own failure boundary, and ECMP across all spines comes for free. It's how hyperscalers build it, and FRR is the BGP daemon inside SONiC, the switch OS in many AI clusters.

Why "unnumbered"

A 64-leaf, 8-spine fabric has 512 links. Numbered, that's 512 /31s to plan, type, and get wrong. Unnumbered BGP peers over the automatic IPv6 link-local address and carries IPv4 routes with IPv6 next hops (RFC 8950). Cable it, name the interface, done.

2The topology

Two spines, three leaves, one host per leaf. The rule to remember: leaf eth0 → spine1, leaf eth1 → spine2, leaf eth2 → host. On a spine, ethN goes to leaf N+1.

eth0eth1eth2 eth0eth1eth2 eth0eth1 eth0eth1 eth0eth1 fabric links: no IPv4 — IPv6 link-local only spine1 AS 65000 · lo 10.0.0.1 spine2 AS 65000 · lo 10.0.0.2 leaf1 AS 65101 · lo 10.0.1.1 leaf2 AS 65102 · lo 10.0.1.2 leaf3 AS 65103 · lo 10.0.1.3 eth2 172.16.1.1 eth2 172.16.2.1 eth2 172.16.3.1 eth0 .10 eth0 .10 eth0 .10 h1 172.16.1.10/24 h2 172.16.2.10/24 h3 172.16.3.10/24 Every leaf connects to every spine → h1 to h3 has 2 equal paths (one per spine)

Why do both spines share AS 65000? A spine never accepts a route that already has its own AS in the path. So spine2 can never learn leaf3's network via leaf1 and spine1. No "valley" paths (leaf → spine → leaf → spine), and no path hunting through the fabric when something fails. That's RFC 7938's design.

3IP & ASN plan

NodeRoleASNLoopback / router-idHost-facing
spine1spine6500010.0.0.1/32—
spine2spine6500010.0.0.2/32—
leaf1leaf6510110.0.1.1/32eth2 172.16.1.1/24
leaf2leaf6510210.0.1.2/32eth2 172.16.2.1/24
leaf3leaf6510310.0.1.3/32eth2 172.16.3.1/24
h1host——172.16.1.10/24, gw .1
h2host——172.16.2.10/24, gw .1
h3host——172.16.3.10/24, gw .1

Fabric links (6) — no IPv4 at all

#Leaf sideSpine side#Leaf sideSpine side
1leaf1 eth0spine1 eth02leaf1 eth1spine2 eth0
3leaf2 eth0spine1 eth14leaf2 eth1spine2 eth1
5leaf3 eth0spine1 eth26leaf3 eth1spine2 eth2

Lab images: FRRouting 10.5.5 containers for the five routers, a small Linux host image (ping, traceroute, iperf3, tcpdump) for h1–h3. The router console opens to a bash shell — type vtysh to get the FRR CLI.

4Config walkthrough

Step 0: kernel prep (every router)

Unnumbered BGP rides on IPv6 link-local addresses, so IPv6 must be on. We also tell Linux to hash ECMP on L4 ports, so parallel flows spread across spines.

/etc/frr/sysctl.sh
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1
sysctl -w net.ipv6.conf.all.disable_ipv6=0
sysctl -w net.ipv6.conf.default.disable_ipv6=0
for i in eth0 eth1 eth2 eth3; do [ -e /proc/sys/net/ipv6/conf/$i ] && sysctl -w net.ipv6.conf.$i.disable_ipv6=0; done
sysctl -w net.ipv4.fib_multipath_hash_policy=1   # hash on src/dst IP + ports

And bgpd=yes and bfdd=yes in /etc/frr/daemons (zebra always runs). In the kit the script lives in /etc/frr/sysctl.sh on every router and runs automatically at boot (the kit's FRR image calls it in GNS3; containerlab runs it as an exec step). If you ever change it, re-run sh /etc/frr/sysctl.sh by hand.

leaf1 — /etc/frr/frr.conf

frr defaults datacenter
Must be line 1. DC-tuned defaults: keepalive 3 s, hold 9 s. That 9 s is the number BFD beats in the demo. Only read at daemon start.
hostname leaf1
Shows up in your neighbors' show bgp summary, since FRR exchanges hostnames.
log stdout
Logs go to the container console.
service integrated-vtysh-config
One frr.conf for all daemons; write memory saves there.
interface lo ip address 10.0.1.1/32
Loopback: a stable address that's also the router-id.
interface eth2 description to-h1 ip address 172.16.1.1/24
The only data interface with an IPv4 address: the host's default gateway. eth0/eth1 get nothing.
bfd profile FAST receive-interval 100 transmit-interval 100 detect-multiplier 3
A BFD timer profile: hello every 100 ms, declare dead after 3 missed = ~300 ms (3 is the default multiplier). Defined but not attached yet — we switch it on live in the demo.
router bgp 65101 bgp router-id 10.0.1.1
Each leaf has its own private ASN (65101, 65102, 65103).
no bgp ebgp-requires-policy
Classic FRR (RFC 8212) won't send or accept eBGP routes without a policy. The datacenter profile already turns that off; we type it anyway so the intent is visible. Production: write real route-maps.
bgp bestpath as-path multipath-relax
Allow ECMP across paths whose AS paths differ but are the same length. Standard fabric setting (see exercise 3 for when it actually matters).
neighbor FABRIC peer-group neighbor FABRIC remote-as external
One template for all uplinks. external = "any ASN that isn't mine" — no need to type the spine's ASN.
neighbor eth0 interface peer-group FABRIC neighbor eth1 interface peer-group FABRIC
This is BGP unnumbered. Peer by interface name. FRR finds the neighbor's IPv6 link-local address through router advertisements and opens the session to it.
address-family ipv4 unicast network 10.0.1.1/32 network 172.16.1.0/24
Advertise the loopback and the host subnet. IPv4 routes ride the IPv6 session with an IPv6 next hop (RFC 8950, extended next hop — FRR enables it automatically on interface peers).
maximum-paths 64
Install up to 64 equal-cost eBGP paths (already FRR's default here; typed for clarity). We only have 2 spines; real AI fabrics have 8, 16, 32+.

leaf2 / leaf3: same file, change hostname, ASN (65102 / 65103), loopback (10.0.1.2 / .3), eth2 (172.16.2.1 / 172.16.3.1) and the two network lines. That's the whole per-leaf diff: three variables.

spine1 — /etc/frr/frr.conf

frr defaults datacenter hostname spine1 log stdout service integrated-vtysh-config
Same header as the leaves.
interface lo ip address 10.0.0.1/32
Loopback only. A spine has no IPv4 on any data port.
bfd profile FAST receive-interval 100 transmit-interval 100 detect-multiplier 3
Same BFD profile, also parked until the demo.
router bgp 65000 bgp router-id 10.0.0.1
Both spines are AS 65000. That's what makes the fabric valley-free.
no bgp ebgp-requires-policy bgp bestpath as-path multipath-relax
Same as the leaves.
neighbor FABRIC peer-group neighbor FABRIC remote-as external neighbor eth0 interface peer-group FABRIC neighbor eth1 interface peer-group FABRIC neighbor eth2 interface peer-group FABRIC
One line per leaf-facing port. Adding a leaf = adding one line. No IPs, no remote ASN.
address-family ipv4 unicast network 10.0.0.1/32 maximum-paths 64
Spines only originate their loopback; they reflect everything else between leaves.

spine2: identical, with hostname spine2 and 10.0.0.2.

What the router actually saved

After write memory, the saved file looks shorter than what we typed. FRR only writes lines that differ from the active defaults, and with frr defaults datacenter, no bgp ebgp-requires-policy, maximum-paths 64 and detect-multiplier 3 are all defaults. Same behavior, fewer lines. This is the real leaf1 file from the lab:

live lab config
frr version 10.5.5_git
frr defaults datacenter
hostname leaf1
log stdout
service integrated-vtysh-config
!
interface eth2
 description to-h1
 ip address 172.16.1.1/24
exit
!
interface lo
 ip address 10.0.1.1/32
exit
!
router bgp 65101
 bgp router-id 10.0.1.1
 bgp bestpath as-path multipath-relax
 neighbor FABRIC peer-group
 neighbor FABRIC remote-as external
 neighbor eth0 interface peer-group FABRIC
 neighbor eth1 interface peer-group FABRIC
 !
 address-family ipv4 unicast
  network 10.0.1.1/32
  network 172.16.1.0/24
 exit-address-family
exit
!
bfd
 profile FAST
  transmit-interval 100
  receive-interval 100
 exit
 !
exit

That's the entire leaf: about two dozen lines, and not one of them is a fabric IP address.

5Verification

The fabric has no IP addresses

live lab output
leaf1# show interface brief
Interface       Status  VRF             Addresses
---------       ------  ---             ---------
eth0            up      default         fe80::42:51ff:fef9:900/64
eth1            up      default         fe80::42:51ff:fef9:901/64
eth2            up      default         172.16.1.1/24
                                        fe80::42:51ff:fef9:902/64
lo              up      default         10.0.1.1/32
pimreg          up      default

eth0 and eth1 carry only an automatic IPv6 link-local address. That's all BGP needs. (pimreg is a PIM placeholder interface in this image — ignore it.)

BGP sessions, by interface name

live lab output
leaf1# show bgp summary

IPv4 Unicast Summary:
BGP router identifier 10.0.1.1, local AS number 65101 VRF default vrf-id 0
BGP table version 12
RIB entries 15, using 1920 bytes of memory
Peers 2, using 33 KiB of memory
Peer groups 1, using 64 bytes of memory

Neighbor        V    AS  MsgRcvd  MsgSent  TblVer  InQ OutQ  Up/Down State/PfxRcd  PfxSnt
spine1(eth0)    4 65000       34       35      12    0    0 00:01:21            5       8
spine2(eth1)    4 65000       35       37      12    0    0 00:01:24            5       8

Total number of neighbors 2

(The Desc column, which shows FRRouting/10.5.5_git, is trimmed here to fit the screen.) Each spine is a 3-neighbor version of the same thing: leaf1(eth0), leaf2(eth1), leaf3(eth2), 2 prefixes each.

live lab output
leaf1# show bgp neighbors eth0
  BGP state = Established, up for 00:01:22
  Hold time is 9 seconds, keepalive interval is 3 seconds
Local host: fe80::42:51ff:fef9:900, Local port: 42968
Foreign host: fe80::42:e8ff:fee3:cb00, Foreign port: 179     # filtered to the key lines

The session runs between two fe80:: addresses, and the hold timer is 9 seconds from frr defaults datacenter. Remember that number.

Spine hostnames, interface names, a prefix count — Established. Leaves show 2 neighbors, spines show 3. If you see Active or Connect, IPv6 is off on that link (check ip -6 addr show dev eth0 for an fe80:: address).

ECMP: two next hops for one prefix

live lab output
leaf1# show bgp ipv4 unicast 172.16.3.0/24
BGP routing table entry for 172.16.3.0/24, version 10
Paths: (2 available, best #1, table default)
  Advertised to peers:
  spine1(eth0) spine2(eth1)
  65000 65103
    fe80::42:75ff:fe5f:c800(spine2) from spine2(eth1) (10.0.0.2)
    (fe80::42:75ff:fe5f:c800) (used)
      Origin IGP, valid, external, multipath, bestpath-from-AS 65000, best (Older Path)
  65000 65103
    fe80::42:e8ff:fee3:cb00(spine1) from spine1(eth0) (10.0.0.1)
    (fe80::42:e8ff:fee3:cb00) (used)
      Origin IGP, valid, external, multipath

Two paths, one per spine, same AS path 65000 65103 (up to a spine, down to leaf3), and both marked multipath.

live lab output
leaf1# show ip route
...
B>* 10.0.0.1/32 [20/0] via fe80::42:e8ff:fee3:cb00, eth0, weight 1, 00:01:21
B>* 10.0.0.2/32 [20/0] via fe80::42:75ff:fe5f:c800, eth1, weight 1, 00:01:24
C>* 10.0.1.1/32 is directly connected, lo, weight 1, 00:01:26
B>* 10.0.1.2/32 [20/0] via fe80::42:75ff:fe5f:c800, eth1, weight 1, 00:01:17
  *                    via fe80::42:e8ff:fee3:cb00, eth0, weight 1, 00:01:17
B>* 10.0.1.3/32 [20/0] via fe80::42:75ff:fe5f:c800, eth1, weight 1, 00:01:21
  *                    via fe80::42:e8ff:fee3:cb00, eth0, weight 1, 00:01:21
C>* 172.16.1.0/24 is directly connected, eth2, weight 1, 00:01:26
B>* 172.16.2.0/24 [20/0] via fe80::42:75ff:fe5f:c800, eth1, weight 1, 00:01:17
  *                      via fe80::42:e8ff:fee3:cb00, eth0, weight 1, 00:01:17
B>* 172.16.3.0/24 [20/0] via fe80::42:75ff:fe5f:c800, eth1, weight 1, 00:01:21
  *                      via fe80::42:e8ff:fee3:cb00, eth0, weight 1, 00:01:21

IPv4 routes with IPv6 next hops. That's RFC 8950 on screen. Every remote leaf prefix has two lines: two stars = both next hops are in the forwarding table. The spine loopbacks have one path each, because each spine is only one hop away on one link.

live lab output
leaf1:~# ip route show 172.16.3.0/24
172.16.3.0/24 nhid 17 proto bgp metric 20
        nexthop via inet6 fe80::42:75ff:fe5f:c800 dev eth1 weight 1
        nexthop via inet6 fe80::42:e8ff:fee3:cb00 dev eth0 weight 1

The Linux kernel agrees: nexthop via inet6, twice.

Valley-free, proven

live lab output
spine2# show bgp ipv4 unicast 172.16.3.0/24
BGP routing table entry for 172.16.3.0/24, version 3
Paths: (1 available, best #1, table default)
  Advertised to peers:
  leaf1(eth0) leaf2(eth1) leaf3(eth2)
  65103
    fe80::42:9fff:fed6:a601(leaf3) from leaf3(eth2) (10.0.1.3)
    (fe80::42:9fff:fed6:a601) (used)
      Origin IGP, metric 0, valid, external, bestpath-from-AS 65103, best (First path received)

Exactly one path. Leaf1 and leaf2 do offer spine2 a copy they learned from spine1, but its AS path contains 65000, so spine2 drops it as a loop.

End to end

live lab output
h1:~# traceroute -n -q1 -w1 -U --sport=40001 172.16.3.10
 1  172.16.1.1  0.213 ms     leaf1
 2  10.0.0.2  0.238 ms       spine2 (answers from its loopback)
 3  10.0.1.3  0.305 ms       leaf3 (loopback, too)
 4  172.16.3.10  0.382 ms    h3

h1:~# traceroute -n -q1 -w1 -U --sport=40003 172.16.3.10
 1  172.16.1.1  0.215 ms
 2  10.0.0.1  0.229 ms       spine1 this time
 3  10.0.1.3  0.311 ms
 4  172.16.3.10  0.381 ms

Leaf, spine, leaf, host: the Clos in four hops. The routers answer from their loopbacks, because the fabric links have no IPv4 address to answer from. And the only thing we changed between the two runs is the UDP source port: hop 2 flips between spine2 and spine1. In the lab, source ports 40001–40006 went 2, 2, 1, 1, 1, 2. That's ECMP hashing on L4 ports, live.

Prove the traffic really spreads

h3:~# iperf3 -s
spine1/2:~# watch -n1 "ip -s link show eth2 | sed -n '4p;6p'"   # eth2 = link to leaf3
h1:~# iperf3 -c 172.16.3.10 -P 8 -t 30

Byte counters climb on both spines. Eight flows, hashed by port number, shared across two spines. That's the point for AI training: every GPU flow gets every spine's bandwidth.

6The demo: a silent failure, without and with BFD

Down vs silent. When a cable is pulled, the interface goes down and BGP reacts instantly. The nasty failure is the one where the light stays on but packets vanish: a dying optic, a one-way fiber fault, a wedged line card. The interface says UP. BGP only notices when its hold timer (9 s) runs out. Until then, traffic hashed onto that path goes into a black hole.

GNS3's Suspend link simulates exactly that: carrier stays up, every packet is dropped. (Don't use Delete link or ip link set down — those are instant failures and spoil the contrast.)

Round 1 — without BFD

  1. Start a fast ping on h1 (10 per second) and leave it running:
    h1:~# ping -i 0.1 172.16.3.10
  2. Find which uplink carries it. ICMP has no ports, so it sticks to one path. In GNS3, right-click link 1 (leaf1 eth0 – spine1 eth0) and link 2 (leaf1 eth1 – spine2 eth0) → Start capture. Whichever shows ICMP is our target. Stop both captures.
    containerlab: sudo scripts/which-uplink.sh compares the packet counters on leaf1 eth0 and eth1 and tells you.
  3. Right-click that link → Suspend. Both interfaces stay up. The ping stops getting replies.
    containerlab: sudo scripts/link.sh suspend eth1 (or eth0) puts 100% packet loss on both ends of the link with containerlab tools netem. Same effect: interfaces up, packets gone.
  4. Watch BGP not notice:
    leaf1# show bgp summary
    Run it a few times. The session to that spine stays Established… for about 9 seconds. Then the hold timer expires, the session drops, and the ping recovers over the other spine.
  5. Ctrl-C the ping and read the loss line.
  6. Resume the link (containerlab: sudo scripts/link.sh resume eth1) and wait for both sessions to come back in show bgp summary.
tested output
--- 172.16.3.10 ping statistics ---
300 packets transmitted, 225 received, 25% packet loss, time 31094ms
# 75 lost = 7.5 seconds of black hole (GNS3 runs: 79 and 71)

BGP held the dead session as Established for 7.6 seconds before giving up. The hold timer expires 9 s after the last keepalive that got through, so where you cut inside the 3 s keepalive cycle sets the exact number: expect 60 to 90.

Round 2 — turn on BFD, live

  1. On leaf1, attach the profile to the whole peer group:
    leaf1:~# vtysh
    leaf1# configure terminal
    leaf1(config)# router bgp 65101
    leaf1(config-router)# neighbor FABRIC bfd profile FAST
    leaf1(config-router)# end
  2. On spine1 and spine2 (eth0 is the link to leaf1 on both):
    spine1# configure terminal
    spine1(config)# router bgp 65000
    spine1(config-router)# neighbor eth0 bfd profile FAST
    spine1(config-router)# end
    BFD needs both ends. We only turn it on for leaf1's two links so the change stays small on camera; in production you'd set neighbor FABRIC bfd profile FAST everywhere.
  3. Shortcut: the same change as one line each, pasted into the router's bash console (tested on both GNS3 and containerlab):
    # leaf1
    vtysh -c 'conf t' -c 'router bgp 65101' -c 'neighbor FABRIC bfd profile FAST' -c 'end'
    # spine1 and spine2
    vtysh -c 'conf t' -c 'router bgp 65000' -c 'neighbor eth0 bfd profile FAST' -c 'end'
  4. Check the BFD sessions (before this step the same command says Session count: 0: BFD really was off):
    live lab output
    leaf1# show bfd peers brief
    Session count: 2
    SessionId  LocalAddress                 PeerAddress                 Status
    =========  ============                 ===========                 ======
    1882543610 fe80::42:51ff:fef9:900       fe80::42:e8ff:fee3:cb00     up
    3591402277 fe80::42:51ff:fef9:901       fe80::42:75ff:fe5f:c800     up
  5. Repeat round 1 exactly: ping -i 0.1, suspend the same link, wait ~10 s, Ctrl-C.
  6. Look at the damage. show bfd peers brief shows one peer down; show bgp summary shows that session already gone; show ip route 172.16.3.0/24 has one next hop.
  7. Resume the link: BFD up, BGP back, two next hops again.
tested output
--- 172.16.3.10 ping statistics ---
200 packets transmitted, 197 received, 1.5% packet loss, time 20696ms
# 3 lost = about 300 ms (GNS3 run: 4)

Resetting for the next take: no neighbor FABRIC bfd under router bgp 65101 on leaf1 and no neighbor eth0 bfd under router bgp 65000 on both spines. Heads-up: removing BFD bounces the BGP sessions for about a second, so don't do it while a ping is running on camera.

~75
packets lost without BFD
BGP hold timer: 9 s
~4
packets lost with BFD
100 ms × 3 = ~300 ms

Same failure. About seventy-five lost packets became four. In a real AI fabric, one stalled path stalls the collective, and the whole training job waits on it. That's the difference between a hiccup and a restarted job.

Why not just set the BGP timers to 1 s? BGP keepalives run in the routing process and get starved when the CPU is busy; BFD is tiny, purpose-built, often offloaded to hardware, and tells every protocol at once.

7Hands-on exercises

Exercise 1 — Scale out with zero IP planning

Add leaf4 (AS 65104, lo 10.0.1.4/32) with h4 on 172.16.4.0/24. Cable leaf4 eth0 → spine1 eth3 and leaf4 eth1 → spine2 eth3. (In the GNS3 project the spines already have a spare eth3. In containerlab, add leaf4, h4 and the three links to the topology file and redeploy.) Goal: h1 pings h4, and leaf1 shows two ECMP next hops for 172.16.4.0/24. How many lines did you change on each spine?

Show answer

leaf4 is the leaf template with four values changed (hostname, ASN, loopback, eth2 subnet). Each spine needs exactly one new line — no IPs, no ASN:

router bgp 65000
 neighbor eth3 interface peer-group FABRIC

Remember the kernel prep on leaf4 (IPv6 on, forwarding on) and eth3 in the spines' sysctl loop. Then on leaf1, show ip route 172.16.4.0/24 shows two fe80:: next hops via eth0 and eth1. The full answer is in the kit under solutions/exercise1/, including a 4-leaf containerlab topology you can deploy as-is. That's the unnumbered payoff: growing the fabric is cabling plus one line per spine.

Exercise 2 — Drain a spine with zero packet loss

Before maintenance on spine2, make the fabric stop using it gracefully. On spine2, build a route-map that prepends the AS path and apply it outbound. Keep the 10 pps ping and the iperf from section 5 running while you do it. Goal: leaf routes collapse to spine1, spine2's counters go flat, and the ping shows 0 loss. Then undo it and watch ECMP return. Why is this lossless when the suspended link wasn't?

Show answer
spine2# configure terminal
spine2(config)# route-map DRAIN permit 10
spine2(config-route-map)#  set as-path prepend 65000 65000
spine2(config-route-map)# exit
spine2(config)# router bgp 65000
spine2(config-router)#  address-family ipv4 unicast
spine2(config-router-af)#   neighbor FABRIC route-map DRAIN out
spine2(config-router-af)# end
spine2# clear bgp ipv4 unicast * soft out

Leaves now see 65000 65103 via spine1 but 65000 65000 65000 65103 via spine2. Multipath (even with multipath-relax) only combines paths of equal AS-path length, so spine2's path is no longer used. Undo with no neighbor FABRIC route-map DRAIN out inside the address family.

Why lossless: it's make-before-break. Spine2 stays up and keeps forwarding while BGP tells everyone to prefer spine1. Traffic moves only after the new path is already installed. The suspended link was break-before-make: packets went into the hole until something noticed.

Exercise 3 — Break the ASN design on purpose

Change spine2 to AS 65001. (a) On spine2, run show bgp ipv4 unicast 172.16.3.0/24: how many paths now, and what are the extra ones? (b) Why didn't the leaves need any config change? (c) Remove bgp bestpath as-path multipath-relax on leaf1, then clear bgp * soft. What happens to ECMP, and why?

Show answer

To change the ASN, remove and re-create the BGP instance: no router bgp 65000, then paste the spine config back with router bgp 65001.

(a) Three paths. The direct one (65103, best) plus two longer ones through other leaves and spine1: 65101 65000 65103 and 65102 65000 65103. Before, spine2 dropped those because 65000 was in the path. Now nothing stops them. These "valley" paths are what cause path hunting: when leaf3's route is withdrawn, spine2 falls back to them one by one before giving up, slowing convergence and briefly routing traffic the wrong way.

(b) The leaves use remote-as external: "any AS that isn't mine." They accept 65001 without being told.

(c) ECMP collapses to one next hop. Without multipath-relax, BGP only load-balances across paths with identical AS paths. Via spine1 it's 65000 65103, via spine2 65001 65103: same length, different ASNs, so only one wins. Note the twist: in the original design (both spines 65000) the paths are identical, so ECMP works even without multipath-relax. Same-ASN spines buy you both valley-freedom and simple multipath.

Two sentences: RFC 7938 puts all spines in one AS so a route can never pass through two spines, which removes valley paths and path hunting. It also makes every leaf-to-leaf path the same AS path, so ECMP just works.

8Recap

Try it yourself

Try it yourself

Download the lab kit (.zip)

Unzip it, then pick one:

Option A — containerlab (one command, no GUI)

Any Linux box (or WSL2) with Docker and containerlab:

$ cd ai-fabric-bgp-unnumbered-lab
$ sudo containerlab deploy -t ai-fabric.clab.yml
$ docker exec -it clab-ai-fabric-leaf1 vtysh        # router CLI
$ docker exec -it clab-ai-fabric-h1 bash             # host shell
$ sudo containerlab destroy -t ai-fabric.clab.yml --cleanup   # when you're done

It uses the stock quay.io/frrouting/frr:10.5.5 and nicolaka/netshoot images, and the interface names match the GNS3 version exactly. GNS3's Suspend link becomes sudo scripts/link.sh suspend eth1.

Option B — GNS3 (the version in the video)

$ docker build -t ai-fabric-lab/frr:10.5.5 docker/frr     # on your GNS3 server / VM

Then in GNS3: File → Import portable project → gns3/ai-fabric-baseline.gns3project, and start all nodes. The routers boot with their configs and kernel settings already in place; BFD is off, ready for the demo. Hosts use gns3/ipterm, which GNS3 pulls by itself.

Needs: about 2 GB of free RAM and 2 CPU cores for the 8 containers (they idle near zero), plus about 1 GB of disk for the images (containerlab) or 300 MB (GNS3). Tested on Ubuntu 24.04 with containerlab 0.73 and GNS3 2.2.56. The README in the kit has the full walkthrough, the demo script, and all exercise answers.