DC Fabric Part 3: Building a Layer 2 EVPN Overlay

host-1 and host-2 share a subnet on different leaves of Part 1’s eBGP fabric. This post builds the VXLAN EVPN overlay that joins them, from VTEP loopbacks and EVPN sessions to the Type 2 and Type 3 routes and the packets on the wire.
37 min read

On this page

Introduction

Part 2 ended on a warning. Our fabric runs eBGP everywhere with a different ASN on every switch, and both choices bend standard EVPN in places where the failure looks the same every time: every BGP session stays up, routes appear in the tables, and host-1 still can’t reach host-2.

This post builds the Layer 2 overlay on Part 1’s eBGP underlay and walks into those bends one at a time. host-1 on LS-1 and host-2 on LS-3 share a VLAN and a /24. By the end they share a broadcast domain across a routed fabric, with VXLAN carrying their frames and EVPN telling each leaf where they are. Every failure comes with the output that shows it, and every output is a capture in the lab repo.

Part 4 adds routing between subnets on top of this overlay.

What This Post Covers

  • The build order for a Layer 2 EVPN overlay, and why each piece depends on the one before it
  • What Part 1’s underlay needs before it can carry VXLAN: MTU and a reachable VTEP address
  • EVPN on its own loopback sessions, and the missing extended communities that leave the overlay dead with every session up
  • What eBGP and a unique ASN per switch change: the spine next hop, route targets, and leaves passing routes between spines
  • What actually crosses the wire: the VXLAN packet, BUM replication and ARP
  • Whether the fabric spreads VXLAN traffic across both spines, and what the lab image can and can’t show

Lab Topology

Lab topology: spines SP-1 (AS 65001) and SP-2 (AS 65002) connect to leaves LS-1, LS-2 and LS-3 (AS 65101 to 65103), each leaf with a Loopback1 VTEP address in 10.255.1.0/24. host-1 (172.16.10.11) on LS-1 and host-2 (172.16.10.12) on LS-3 share VLAN 10 and VNI 10010; LS-2 is in the VNI with no host.

The fabric underneath is Part 1’s, unchanged: two spines, three leaves, eBGP over /31 links with the spine on .0, and a unique private ASN on every switch. Part 3 adds three things. host-1 sits on LS-1’s Ethernet3 and host-2 on LS-3’s Ethernet3, both in VLAN 10 and 172.16.10.0/24. Each leaf gets a Loopback1 from 10.255.1.0/24 as its VTEP address. VLAN 10 maps to VNI 10010 on all three leaves.

LS-2 joins the VNI with no host behind it. That turns out to be useful: it shows what a leaf advertises when it has nothing to report but its membership. The hosts use .11 and .12, which keeps .1 free for the gateway in Part 4.

The running example is the one from Part 2: host-1 talking to host-2. Every section comes back to that one conversation.

The lab runs on netlab with the containerlab provider, Arista cEOS 4.36.2F on the switches and netlab’s default Linux containers as hosts. Each cEOS node used about 1 GB of RAM and close to half a CPU core at idle; the two hosts together used less than 30 MB. The topology, templates, scripts and every capture quoted here are in the lab repo. netlab up builds the finished state. Remove the five Part 3 templates from topology.yml and it builds the underlay and hosts only, ready for the overlay to be configured by hand.

The Build, in Order

The overlay is built in seven steps, and the order is not arbitrary: each step depends on the one before it, and a broken lower step looks exactly like a broken overlay. The rest of the post follows this order.

Seven build steps for the Layer 2 EVPN overlay, each with its check: underlay, MTU, VTEP address, EVPN sessions, VXLAN and MAC-VRF, a no-transit policy on the leaves, and end-to-end verification from host-1 to host-2.
  1. Underlay. Part 1’s fabric: every Loopback0 reachable, two paths from each leaf to every other leaf. The EVPN sessions in step 4 ride on Lo0 reachability.
  2. MTU. Every fabric link carries the largest host frame plus 50 bytes, on spines and leaves. Done first because a wrong MTU fails silently and later, on large packets only.
  3. VTEP address. Loopback1 on each leaf, advertised into the underlay, and Part 1’s prefix-lists opened for the VTEP block on spines and leaves. Every EVPN route will name this address as its next hop, so it has to be reachable before any route arrives.
  4. EVPN sessions. Loopback0 to Loopback0 eBGP from each leaf to both spines, EVPN address family only, extended communities sent; next-hop-unchanged on the spines.
  5. VXLAN and MAC-VRF. On each leaf: Vxlan1 sourced from Loopback1 with VLAN 10 mapped to VNI 10010, and the vlan 10 block under BGP with an RD, the shared route target and redistribute learned.
  6. No transit. An outbound policy on the leaves’ EVPN sessions, so a leaf only advertises routes it originated.
  7. Verify. host-1 pings host-2; LS-3 lists host-1’s MAC on Vx1 and both route types carry the route target.

In short: underlay, MTU and VTEP reachability come before any EVPN config. Prove each step with its check before starting the next.

MTU Before Anything Else

VXLAN adds 50 bytes to every host frame, and a VTEP must not fragment it. If the fabric links can’t carry a full 1500-byte host packet plus those 50 bytes, small pings work and large ones vanish, which looks like an EVPN fault and isn’t one. So the first check happens before any overlay config exists.

netlab had left every fabric link at 1500:

EOS
LS-1#show ip interface brief
Interface      IP Address             Status     Protocol         MTU 
-------------- ---------------------- ---------- ------------ ---------
Ethernet1      10.1.11.1/31           up         up              1500
Ethernet2      10.2.11.1/31           up         up              1500
Loopback0      10.11.11.11/32         up         up             65535
Management0    192.168.121.103/24     up         up              1500

Arista’s EVPN sample configuration uses 9214 on fabric links. netlab wouldn’t build it:

[ERRORS]  Errors found in topology.yml
[VALUE]   nodes: Node MTU 9214 on node SP-1 is higher than the maximum MTU for eos (9194)
(the same error follows for SP-2, LS-1, LS-2 and LS-3)

netlab caps EOS at 9194, which still leaves room for any host frame plus VXLAN. The value goes on the two switch groups only, not as a lab-wide default. A global default would also reach the Linux hosts, and a host at 9194 can send a frame that no longer fits once VXLAN adds its 50 bytes. The hosts stay at 1500.

YAML
groups:
  spines:
    members: [ SP-1, SP-2 ]
    mtu: 9194
  leaves:
    members: [ LS-1, LS-2, LS-3 ]
    mtu: 9194

After a rebuild, the links show 9194 and a 9000-byte ping with the DF bit set crosses the fabric from LS-1’s loopback to LS-3’s:

EOS
LS-1#ping 10.13.13.13 source 10.11.11.11 size 9000 df-bit repeat 2
PING 10.13.13.13 (10.13.13.13) from 10.11.11.11 : 8972(9000) bytes of data.
8980 bytes from 10.13.13.13: icmp_seq=1 ttl=63 time=1.42 ms
8980 bytes from 10.13.13.13: icmp_seq=2 ttl=63 time=0.640 ms

--- 10.13.13.13 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 3ms
! output trimmed

The TTL of 63 means one router in between, a spine. One ping proves one of the two paths, since ECMP sends a flow one way only; the same setting is on every switch, so the other path matches.

In short: check MTU before any overlay config. The fabric links need host MTU plus 50 bytes; on this lab that’s 9194 on the switches and 1500 on the hosts, proven with a DF ping.

Giving Each Leaf a VTEP

Every EVPN route a leaf sends will name its Loopback1 as the next hop, and every VXLAN packet it builds will use Loopback1 as the outer source. So each leaf’s VTEP address has to be reachable from every other leaf before any EVPN route exists. Part 1’s underlay policy was written to carry Loopback0 and nothing else, and it does exactly that.

On LS-1, Loopback1 and a network statement put the route into BGP:

EOS
interface Loopback1
   description VTEP
   ip address 10.255.1.11/32
router bgp 65101
   address-family ipv4
      network 10.255.1.11/32

The route is in LS-1’s own table, valid and best, and goes nowhere:

EOS
LS-1#show ip bgp 10.255.1.11/32
BGP routing table entry for 10.255.1.11/32
 Paths: 1 available
  Local
    - from - (10.11.11.11)
      Origin IGP, metric -, localpref -, IGP metric -, weight 0, tag 0
      Received 00:00:34 ago, valid, local, best, redistributed (Connected)
! output trimmed

LS-1#show ip bgp neighbors 10.1.11.0 advertised-routes
            Network                Next Hop              Metric  AIGP       LocPref Weight  Path
 * >        10.11.11.11/32         10.1.11.1             -       -          -       -       65101 i
! output trimmed

There are four filters on the way to LS-3, one at each end of each session, and each of them drops the route without a log message or a session reset:

The route for LS-1’s VTEP, 10.255.1.11/32, passes four filters on its way to LS-3: LS-1 out, SP-1 in, SP-1 out and LS-3 in. Each needs a VTEP entry added to Part 1’s prefix-lists.

Opening only the first one shows where the next one sits. With seq 20 permit 10.255.1.11/32 added to LS-1’s outbound list, LS-1 advertises the route and SP-1 throws it away on arrival: two prefixes received, one accepted.

EOS
SP-1#show ip bgp summary
  Description              Neighbor  V AS           MsgRcvd   MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc PfxAdv
  LS-1                     10.1.11.1 4 65101             25        22    0    0 00:13:01 Estab   2      1      3
  LS-2                     10.1.12.1 4 65102             27        26    0    0 00:13:01 Estab   1      1      3
  LS-3                     10.1.13.1 4 65103             27        26    0    0 00:13:01 Estab   1      1      3
! output trimmed

Part 1 used exact /32 entries and named their cost: every new leaf means editing lists on every switch. For the VTEP block I took a different route. Each leaf advertises its own VTEP as an exact /32 on the way out, and every other filter accepts one range, 10.255.1.0/24 ge 32, any /32 inside the block. A new leaf touches no other switch’s policy, and a leaf still can’t advertise anything outside its own address.

With Loopback1 on all three leaves and the range on all four filter points, every leaf reaches every VTEP over both spines, and the jumbo ping from the MTU check now works between VTEP addresses too:

EOS
LS-1#show ip route 10.255.1.13
 B E      10.255.1.13/32 [200/0]
           via 10.1.11.0, Ethernet1
           via 10.2.11.0, Ethernet2
! output trimmed

LS-1#ping 10.255.1.13 source 10.255.1.11 size 9000 df-bit repeat 2
8980 bytes from 10.255.1.13: icmp_seq=1 ttl=63 time=1.04 ms
8980 bytes from 10.255.1.13: icmp_seq=2 ttl=63 time=0.675 ms
! output trimmed

In short: the VTEP address must be reachable before EVPN starts. Part 1’s exact-/32 policy blocks it at four points without a word; one range entry for the VTEP block opens all four and scales with the fabric.

EVPN on Its Own Sessions

EVPN routes need BGP sessions to travel on, and Part 1 already has six of them, one per spine-leaf link across its /31. EVPN could ride those. It doesn’t here.

Opinion: I’m putting the overlay on its own loopback sessions. Part 1’s underlay neighbors carry exact-/32 policy and next-hop-self, all tuned for IPv4. Keeping EVPN off those neighbors means none of that can leak into the overlay, and it follows Arista’s documented design, so the docs and my lab describe the same thing. The cost is real: six more BGP sessions, and an overlay that depends on loopback reachability through the underlay it sits on. iBGP over the eBGP underlay is out for this series; it would mean reworking the ASN plan Part 1 just justified.

Not everyone agrees. Ivan Pepelnjak argues against this exact design: eBGP EVPN sessions between loopbacks, over an eBGP underlay, keep the drawbacks of eBGP-only EVPN and add a loopback reachability problem. His recommendation is iBGP over an IGP. Several of the problems in the rest of this post come straight from that combination of eBGP and a different ASN on every switch.

Each leaf peers from its Loopback0 with both spines’ Loopback0, and each spine with all three leaves. Loopback0, not Loopback1: the session is control plane, the VTEP address is data plane. A session between loopbacks isn’t between directly connected interfaces, so it needs ebgp-multihop. On LS-1:

EOS
router bgp 65101
   neighbor EVPN peer group
   neighbor EVPN update-source Loopback0
   neighbor EVPN ebgp-multihop 3
   neighbor 10.1.1.1 peer group EVPN
   neighbor 10.1.1.1 remote-as 65001
   neighbor 10.1.1.1 description SP-1-EVPN
   neighbor 10.2.2.2 peer group EVPN
   neighbor 10.2.2.2 remote-as 65002
   neighbor 10.2.2.2 description SP-2-EVPN
   address-family evpn
      neighbor EVPN activate

The spines mirror it, one neighbor per leaf. Nothing activates these neighbors for IPv4 unicast, and nothing has to turn it off: netlab’s generated config already contains no bgp default ipv4-unicast, so the new sessions carry EVPN only and can’t become a side door around Part 1’s IPv4 policy. EVPN on EOS also needs the multi-agent routing model; show running-config all | include service routing protocols returned service routing protocols model multi-agent on cEOS 4.36.2F, so no reload.

This is the lab’s first version of the sessions, and it leaves out one line Arista’s sample configuration has: send-community extended. The next section is about that line.

EOS
SP-1#show bgp evpn summary
  Description              Neighbor    V AS           MsgRcvd   MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc PfxAdv
  LS-1-EVPN                10.11.11.11 4 65101              5         5    0    0 00:01:16 Estab   0      0      0
  LS-2-EVPN                10.12.12.12 4 65102              5         5    0    0 00:01:06 Estab   0      0      0
  LS-3-EVPN                10.13.13.13 4 65103              4         4    0    0 00:00:48 Estab   0      0      0
! output trimmed

All Established, all empty. There’s no VNI yet, so no leaf has anything to advertise.

In short: EVPN runs on separate eBGP sessions between Loopback0 addresses, EVPN address family only. They come up empty until the leaves have a VNI to advertise.

Routes Arrive, Nothing Works

Two pieces turn a leaf into a VTEP for VLAN 10. Vxlan1 is the data plane: it sources tunnels from Loopback1 and maps VLAN 10 to VNI 10010. The vlan 10 block under BGP is the MAC-VRF from Part 2: an RD, a route target, and redistribute learned, which turns every MAC the leaf learns locally into a Type 2 route. On LS-1:

EOS
interface Vxlan1
   vxlan source-interface Loopback1
   vxlan udp-port 4789
   vxlan vlan 10 vni 10010
!
router bgp 65101
   vlan 10
      rd auto
      route-target both 10010:10010
      redistribute learned

LS-3 gets the same with its own ASN. LS-2 also needs vlan 10 created first: netlab only builds the VLAN where a host is attached.

The route target is 10010:10010 on every leaf, as Part 2 promised: one RT per VNI, shared by all. Each leaf now advertises a Type 3 route for VNI 10010, and host-1’s first frame gives LS-1 a Type 2. Both reach LS-3 through both spines, valid and best. They’re also useless. Compare LS-3’s own Type 3 with the one it received from LS-1:

EOS
LS-3#show bgp evpn route-type imet detail
BGP routing table entry for imet 10.255.1.11, Route Distinguisher: 10.11.11.11:10
 Paths: 2 available
  65001 65101
    10.255.1.11 from 10.1.1.1 (10.1.1.1)
      Origin IGP, metric -, localpref 100, weight 0, tag 0, valid, external, ECMP head, ECMP, best, ECMP contributor
      VNI: 10010
      PMSI Tunnel: Ingress Replication, VNI: 10010, Leaf Information Required: false, Tunnel ID: 10.255.1.11
! output trimmed
BGP routing table entry for imet 10.255.1.13, Route Distinguisher: 10.13.13.13:10
 Paths: 1 available
  Local
    - from - (0.0.0.0)
      Origin IGP, metric -, localpref -, weight 0, tag 0, valid, local, best
      Extended Community: Route-Target-AS:10010:10010 TunnelEncap:tunnelTypeVxlan
      VNI: 10010
      PMSI Tunnel: Ingress Replication, VNI: 10010, Leaf Information Required: false, Tunnel ID: 10.255.1.13

LS-3’s local route carries Route-Target-AS:10010:10010 TunnelEncap:tunnelTypeVxlan. The received one has no Extended Community line at all. The same goes for host-1’s Type 2: on LS-1 it carries both communities, on LS-3 it carries none. Part 2 warned about this: without the route target there is nothing to import the route with, and without the encapsulation community there is no VXLAN tunnel to build. EOS doesn’t send extended communities to a BGP neighbor unless told to, and none of the five switches was told.

The route target config itself is correct, which rules out the obvious suspect:

EOS
LS-3#show bgp evpn instance
EVPN instance: VLAN 10
  Route distinguisher: 10.13.13.13:10
  Route target import: Route-Target-AS:10010:10010
  Route target export: Route-Target-AS:10010:10010
! output trimmed

LS-3#show vxlan vtep
Remote VTEPS for Vxlan1:

VTEP       Tunnel Type(s)
---------- --------------

Total number of remote VTEPS:  0

No remote VTEPs, an empty flood list, and host-1’s ping to host-2 fails with 100% loss, while every BGP session in the fabric is up and nothing has logged an error. This is the failure Part 2 promised, in its plainest form.

The fix is one line on all five switches. The spines need it too: they forward the leaves’ routes, and a spine without it strips the communities on the way through.

EOS
router bgp 65001
   neighbor EVPN send-community extended

The received routes now carry Route-Target-AS:10010:10010 TunnelEncap:tunnelTypeVxlan, LS-3 lists two remote VTEPs, and the ping works.

In short: EVPN routes without extended communities arrive valid and best, and nothing imports them. send-community extended belongs on every EVPN neighbor, spines included.

Following host-1’s MAC

With the communities in place, the two route types from Part 2 do exactly what Part 2 said they would, and LS-3’s tables show each one’s job.

The Type 3 routes build the flood list. Every leaf in VNI 10010 advertises one, and the set a leaf receives is where it sends broadcast, unknown unicast and multicast for that VNI:

EOS
LS-3#show vxlan flood vtep
VLANS                            Ip Address
-----------------------------   ------------------------------------------------
10                              10.255.1.11     10.255.1.12
! output trimmed

LS-2 is on the list. It has no host, but it is in the VNI, so it gets a copy of every flooded frame. Nobody configured that list; LS-2 joined it by advertising one route.

The Type 2 routes carry the MACs. host-1’s MAC arrives twice, once through each spine, with the same next hop: LS-1’s VTEP.

EOS
LS-3#show bgp evpn route-type mac-ip detail
BGP routing table entry for mac-ip aac1.ab47.1dfb, Route Distinguisher: 10.11.11.11:10
 Paths: 2 available
  65002 65101
    10.255.1.11 from 10.2.2.2 (10.2.2.2)
      Origin IGP, metric -, localpref 100, weight 0, tag 0, valid, external, ECMP head, ECMP, best, ECMP contributor
      Extended Community: Route-Target-AS:10010:10010 TunnelEncap:tunnelTypeVxlan
      VNI: 10010 ESI: 0000:0000:0000:0000:0000
  65001 65101
    10.255.1.11 from 10.1.1.1 (10.1.1.1)
      Origin IGP, metric -, localpref 100, weight 0, tag 0, valid, external, ECMP, ECMP contributor
      Extended Community: Route-Target-AS:10010:10010 TunnelEncap:tunnelTypeVxlan
      VNI: 10010 ESI: 0000:0000:0000:0000:0000
! output trimmed

LS-3#show mac address-table vlan 10
Vlan    Mac Address       Type        Ports      Moves   Last Move
----    -----------       ----        -----      -----   ---------
  10    aac1.ab47.1dfb    DYNAMIC     Vx1        1       0:01:16 ago
  10    aac1.abbe.04fa    DYNAMIC     Et3        1       0:00:45 ago
! output trimmed

host-1’s MAC sits on Vx1, installed from BGP rather than learned from traffic; host-2’s sits on the access port. The route is mac-ip by name but carries a MAC only. With no gateway address on the leaves, there is no MAC-to-IP binding to advertise. That detail comes back in the ARP section.

One table shows both route types at a glance:

EOS
LS-3#show vxlan vtep
Remote VTEPS for Vxlan1:

VTEP              Tunnel Type(s)
----------------- --------------
10.255.1.11       flood, unicast
10.255.1.12       flood

Total number of remote VTEPS:  2

10.255.1.11 is flood, unicast: LS-1 sent a Type 3 and a Type 2. 10.255.1.12 is flood only: LS-2 sent a Type 3 and has no MACs to report.

And host-1 reaches host-2:

Alpine Linux
ping -c 3 172.16.10.12
PING 172.16.10.12 (172.16.10.12): 56 data bytes
64 bytes from 172.16.10.12: seq=0 ttl=64 time=14.545 ms
64 bytes from 172.16.10.12: seq=1 ttl=64 time=2.623 ms
64 bytes from 172.16.10.12: seq=2 ttl=64 time=4.929 ms
! output trimmed

The TTL is 64, untouched: host-1’s frame was bridged at both leaves and never routed; the spine routed only the outer VXLAN packet. In Part 4, routed traffic between subnets should show two decrements instead.

In short: Type 3 routes build each leaf’s flood list, Type 2 routes install remote MACs on the VXLAN interface. show vxlan vtep shows both: flood for VNI membership, unicast for MACs behind a VTEP.

The Spine Next Hop

RFC 8365 requires the BGP next hop of an EVPN route to be the advertising leaf’s VTEP, because the receiving leaf uses it as the outer destination of every VXLAN packet. eBGP has the opposite habit: a router advertising a route to an eBGP neighbor normally puts its own address in the next hop. Part 1 even sets next-hop-self on the underlay sessions. If a spine did that to an EVPN route, LS-3 would build its tunnel to SP-1, and SP-1 isn’t a VTEP.

Arista’s sample configuration handles this with next-hop-unchanged on the spines’ EVPN neighbors. The first build left it out on purpose, and my prediction going in was that the spines would rewrite the next hop to their own Loopback0.

They didn’t. The capture from the communities section already showed it, before the communities were even fixed:

EOS
LS-3#show bgp evpn route-type imet detail
BGP routing table entry for imet 10.255.1.11, Route Distinguisher: 10.11.11.11:10
 Paths: 2 available
  65001 65101
    10.255.1.11 from 10.1.1.1 (10.1.1.1)
! output trimmed

10.255.1.11 from 10.1.1.1: learned from SP-1, next hop still LS-1’s VTEP. And SP-1’s configuration at that point had no next-hop command on the EVPN peer group at all:

EOS
SP-1#show running-config section router bgp
router bgp 65001
   router-id 10.1.1.1
   no bgp default ipv4-unicast
   bgp advertise-inactive
   neighbor EVPN peer group
   neighbor EVPN update-source Loopback0
   neighbor EVPN ebgp-multihop 3
! output trimmed
   address-family evpn
      neighbor EVPN activate
! output trimmed

On cEOS 4.36.2F, the spines passed the leaf’s VTEP through unchanged on eBGP EVPN sessions without being told to. To make sure the command really is redundant here and not doing something subtle, I added it to both spines and compared:

EOS
router bgp 65001
   neighbor EVPN next-hop-unchanged

Nothing changed. LS-3’s Type 3 and Type 2 routes showed the same next hops, the same communities and the same two paths as before, and the ping kept working.

Opinion: the command stays in the final configuration anyway. It’s what Arista documents, it states the spines’ job in the config instead of leaving it to a default, and a default that holds on one release is a weak thing to build on. If a future EOS version changed this behavior, the overlay would break in the quietest possible way: sessions up, routes present, tunnels pointed at a spine.

In short: an EVPN next hop must stay the originating leaf’s VTEP. On cEOS 4.36.2F the spines kept it without next-hop-unchanged; it goes in the config anyway, so the design doesn’t depend on a default.

Route Targets With a Different ASN on Every Leaf

Part 2 said a shared route target “sounds trivial. With a different ASN on every switch it isn’t.” The manual 10010:10010 works. The interesting question is what happens when you let EOS pick the route target.

EOS offers it under import only, and the help output already tells most of the story:

EOS
LS-3(config-macvrf-10)#route-target both ?
  ASN(asplain):nn or ASN(asdot):nn or IP-address:nn  Route target

LS-3(config-macvrf-10)#route-target import ?
  ASN(asplain):nn or ASN(asdot):nn or IP-address:nn  Route target
  any-as:nn                                          Wildcard 2-byte AS Route Target
  auto                                               Auto Generate Route Target
  evpn                                               Configure route target for EVPN address family
  export                                             Route Export

LS-3(config-macvrf-10)#route-target import auto ?
  <1-4294967295>       Autonomous system number
  <1-65535>.<0-65535>  AS Number in asdot notation

route-target export auto is accepted as is. route-target import auto wants an ASN: the automatic RT of which switch. To test it, LS-3 got the setup a single-ASN fabric would use, exporting its automatic RT and importing the automatic RT of its own ASN:

EOS
router bgp 65103
   vlan 10
      route-target export auto
      route-target import auto 65103
EOS
LS-3#show bgp evpn instance
EVPN instance: VLAN 10
  Route distinguisher: 10.13.13.13:10
  Route target import: Route-Target-AS:65103:268445466 (auto, VNI 10010)
  Route target export: Route-Target-AS:65103:268445466 (auto, VNI 10010)
! output trimmed

The value isn’t 65103:10010. 268445466 is 0x1000271A: the top four bits are 1, the type field RFC 8365 defines for automatically derived RTs, meaning VXLAN, and the low 24 bits are 0x271A, which is 10010, the VNI. EOS follows RFC 8365’s encoding exactly. The ASN half comes from the switch’s own ASN, so on this fabric no two leaves can ever produce the same value.

LS-1 received LS-3’s Type 3 carrying Route-Target-AS:65103:268445466, valid and best, and didn’t import it. LS-3 dropped to zero remote VTEPs, host-1’s MAC disappeared from its table and the ping failed. Every BGP session stayed up.

Automatic RTs can still work here. The wildcard ignores the ASN half, so every leaf can export its automatic RT and import one line that matches all of them:

EOS
router bgp 65101
   vlan 10
      route-target export auto
      route-target import any-as:268445466

With that on all three leaves, LS-1 imported routes carrying 65102:268445466 and 65103:268445466, LS-3 listed both remote VTEPs again and the ping worked. The wildcard has to name the encoded value, 268445466, not the VNI, so read the automatic RT off show bgp evpn instance before writing it.

Route target setupResult on this fabric
route-target both 10010:10010 on every leafWorks
export auto + import auto <own ASN>Fails silently: each leaf’s RT carries its own ASN
export auto + import auto <ASN> per remote leafNot tested; every leaf would list every other leaf’s ASN
export auto + import any-as:268445466Works

The third row is Part 1’s scaling problem again: with a unique ASN per leaf, every new leaf means editing every other leaf, just like the exact-/32 prefix-lists. In a fabric where all leaves share an ASN, it would be one line.

Opinion: I’m keeping the manual shared RT. It’s what Part 2 committed to, every leaf’s config reads the same, and the value means something when you see it in a route. The wildcard is a fair alternative, but it hides a magic number that only makes sense after decoding RFC 8365.

In short: EOS builds automatic RTs from the local ASN and RFC 8365’s encoding, so with a different ASN on every leaf they never match. Use one shared manual RT, or export auto with an any-as wildcard import.

Leaves as Transit, Again

Part 1 made sure a leaf never passes one spine’s routes to the other: RM-UNDERLAY-OUT lets a leaf advertise only its own Loopback0. That policy lives in the IPv4 address family. The EVPN sessions have no policy at all, and with a different ASN on every spine, nothing else stops it either.

The first sign was in LS-1’s EVPN summary, which didn’t add up. LS-1 originates one route, its Type 3, yet it advertised three to SP-1 and one to SP-2:

EOS
LS-1#show bgp evpn summary
  Description              Neighbor V AS           MsgRcvd   MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc PfxAdv
  SP-1-EVPN                10.1.1.1 4 65001             12        13    0    0 00:05:56 Estab   2      2      3
  SP-2-EVPN                10.2.2.2 4 65002             12        11    0    0 00:05:56 Estab   2      2      1
! output trimmed

LS-1 had picked SP-2’s paths as best for LS-2’s and LS-3’s routes, and a BGP speaker advertises its best paths to its other neighbors. So LS-1 handed SP-2’s routes to SP-1. SP-1 then held paths through a leaf, including one for LS-1’s own route that had traveled LS-1, SP-2, LS-2, SP-1:

EOS
SP-1#show bgp evpn route-type imet detail
BGP routing table entry for imet 10.255.1.11, Route Distinguisher: 10.11.11.11:10
 Paths: 2 available
  65101
    10.255.1.11 from 10.11.11.11 (10.11.11.11)
      Origin IGP, metric -, localpref 100, weight 0, tag 0, valid, external, best
  65102 65002 65101
    10.255.1.11 from 10.12.12.12 (10.12.12.12)
      Origin IGP, metric -, localpref 100, weight 0, tag 0, valid, external
! output trimmed

In a fabric where both spines share one ASN, that second path would carry the spines’ own ASN, and SP-1 would drop it on arrival as a loop. Here the spines are 65001 and 65002, so 65102 65002 65101 looks perfectly valid to SP-1. It isn’t harmful today. The direct path wins on AS_PATH length, and the next hop is still the VTEP, so the data plane is unaffected. It’s extra state on every spine, and the same failure mode Part 1 designed out, back in a new address family.

It also moves around. After the route-target experiments, LS-1’s best paths flipped to SP-1, and the extra copies went to SP-2 instead: LS-1 advertised 2 routes to SP-1 and 5 to SP-2, while originating 2 (its Type 3 and host-1’s Type 2). Which spine collects the transit copies depends on the leaf’s tiebreaks at that moment.

The fix is the EVPN version of Part 1’s rule: a leaf advertises only what it originated. A locally originated route has an empty AS_PATH, so one AS-path list and one route-map on each leaf’s EVPN sessions does it:

EOS
ip as-path access-list AS-LOCAL permit ^$ any
!
route-map RM-EVPN-OUT permit 10
   match as-path AS-LOCAL
!
router bgp 65101
   address-family evpn
      neighbor EVPN route-map RM-EVPN-OUT out

After it, LS-1 advertised 2 routes to each spine, and each spine received exactly what each leaf originates:

EOS
SP-2#show bgp evpn summary
  Description              Neighbor    V AS           MsgRcvd   MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc PfxAdv
  LS-1-EVPN                10.11.11.11 4 65101             70        79    0    0 00:43:49 Estab   2      2      3
  LS-2-EVPN                10.12.12.12 4 65102             80        81    0    0 00:43:38 Estab   1      1      4
  LS-3-EVPN                10.13.13.13 4 65103             75        77    0    0 00:43:20 Estab   2      2      3
! output trimmed

LS-1 and LS-3 each originate a Type 3 and their host’s Type 2; LS-2 only its Type 3. Both spines now hold one path per route, from the leaf that originated it, and the leaves still see two paths for every remote route, one through each spine. The filter removed the transit copies, not the redundancy.

In short: without the underlay’s outbound policy, leaves pass EVPN routes between spines, and unique spine ASNs mean AS_PATH doesn’t stop it. An ^$ AS-path filter on the leaves’ EVPN sessions restores Part 1’s no-transit rule.

On the Wire

Everything so far came from BGP tables and MAC tables. The packets themselves are one capture away. tcpdump runs from the lab host inside LS-1’s container namespace, on its two uplinks, while host-1 clears its ARP cache and pings host-2 three times. Inside a cEOS container the ports are et1 and et2, not eth1; the capture script is in the repo as scripts/capture-vxlan.sh.

The Packet

Here is host-1’s first echo request as it left LS-1 toward SP-1:

TCPDUMP
11:14:04.818295 ca:f0:00:03:00:01 > ca:f0:00:01:00:01, ethertype IPv4 (0x0800), length 148: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto UDP (17), length 134)
    10.255.1.11.7928 > 10.255.1.13.4789: VXLAN, flags [I] (0x08), vni 10010
aa:c1:ab:47:1d:fb > aa:c1:ab:be:04:fa, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13466, offset 0, flags [DF], proto ICMP (1), length 84)
    172.16.10.11 > 172.16.10.12: ICMP echo request, id 184, seq 0, length 64
VXLAN packet anatomy from the capture: outer Ethernet (14 bytes), outer IPv4 10.255.1.11 to 10.255.1.13 (20 bytes), outer UDP source 7928 to 4789 (8 bytes) and VXLAN VNI 10010 (8 bytes) around host-1’s unchanged 98-byte frame, 148 bytes on the wire.

The first two lines are LS-1’s work, the last two are host-1’s frame. The overhead is exactly Part 2’s arithmetic: a 98-byte frame went in, a 148-byte packet went out. The outer source and destination are the two VTEP addresses, the destination port is 4789, the I flag marks the VNI as valid, and the DF bit is set, which is why the MTU check came first. The outer TTL leaves LS-1 at 64 and arrives at LS-3 at 63, one spine later; the inner TTL never changes.

The First ARP

host-1 had just flushed its ARP cache, so the ping started with a broadcast. LS-1 doesn’t know who has 172.16.10.12; it floods, and on an EVPN fabric flooding means one unicast copy to every VTEP on the flood list:

TCPDUMP
11:14:04.812810 ca:f0:00:03:00:01 > ca:f0:00:01:00:01, ethertype IPv4 (0x0800), length 110: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto UDP (17), length 96)
    10.255.1.11.58242 > 10.255.1.13.4789: VXLAN, flags [I] (0x08), vni 10010
aa:c1:ab:47:1d:fb > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60: Ethernet (len 6), IPv4 (len 4), Request who-has 172.16.10.12 tell 172.16.10.11, length 46
11:14:04.812838 ca:f0:00:03:00:01 > ca:f0:00:01:00:01, ethertype IPv4 (0x0800), length 110: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto UDP (17), length 96)
    10.255.1.11.58242 > 10.255.1.12.4789: VXLAN, flags [I] (0x08), vni 10010
aa:c1:ab:47:1d:fb > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60: Ethernet (len 6), IPv4 (len 4), Request who-has 172.16.10.12 tell 172.16.10.11, length 46
11:14:04.817064 ca:f0:00:01:00:01 > ca:f0:00:03:00:01, ethertype IPv4 (0x0800), length 110: (tos 0x0, ttl 63, id 0, offset 0, flags [DF], proto UDP (17), length 96)
    10.255.1.13.6648 > 10.255.1.11.4789: VXLAN, flags [I] (0x08), vni 10010
aa:c1:ab:be:04:fa > aa:c1:ab:47:1d:fb, ethertype ARP (0x0806), length 60: Ethernet (len 6), IPv4 (len 4), Reply 172.16.10.12 is-at aa:c1:ab:be:04:fa, length 46
host-1’s ARP broadcast leaves LS-1 as two VXLAN copies, one to LS-3 and one to LS-2. LS-3 delivers it to host-2, and host-2’s reply returns as a single unicast tunnel to LS-1.

One broadcast left LS-1 as two packets, 28 microseconds apart: this is ingress replication, Part 2’s head-end copy per remote VTEP, built from the Type 3 routes. LS-2 got its copy even though nobody behind it could answer. The reply came back once, unicast, because LS-3 already had host-1’s MAC from a Type 2 route.

The Second ARP

Part 2 left an open question: once a host’s MAC and IP arrive as a Type 2 route, the next ARP for it can be answered by the local leaf, if cEOS does that by default. Here the answer was decided earlier, by show bgp evpn route-type mac-ip: the Type 2 routes carry a MAC and no IP, so there is no binding to answer from.

To check, host-1 flushed its ARP cache twice, five seconds apart, pinging host-2 after each flush, while tcpdump listened on host-2’s own interface:

TCPDUMP
11:01:57.890945 aa:c1:ab:47:1d:fb > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60: Request who-has 172.16.10.12 tell 172.16.10.11, length 46
11:01:57.890954 aa:c1:ab:be:04:fa > aa:c1:ab:47:1d:fb, ethertype ARP (0x0806), length 42: Reply 172.16.10.12 is-at aa:c1:ab:be:04:fa, length 28
11:02:02.431873 aa:c1:ab:47:1d:fb > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60: Request who-has 172.16.10.12 tell 172.16.10.11, length 46
11:02:02.431881 aa:c1:ab:be:04:fa > aa:c1:ab:47:1d:fb, ethertype ARP (0x0806), length 42: Reply 172.16.10.12 is-at aa:c1:ab:be:04:fa, length 28
! output trimmed

Both requests reached host-2, and host-2 answered both. No leaf answered on its behalf. In a pure Layer 2 segment, every ARP floods. Part 4 puts gateway addresses on the leaves, and with them MAC-and-IP Type 2 routes, which is where ARP suppression can appear.

Unknown Unicast After a Quiet Spell

The last kind of flooding showed up by accident. After nine minutes without traffic, during the ECMP test in the next section, the first echo request from host-1 (sent from one of its test addresses, 172.16.10.21) left LS-1 twice, to both VTEPs, even though it was addressed to host-2’s MAC:

TCPDUMP
11:23:16.853452 IP 10.255.1.11.248 > 10.255.1.13.4789: VXLAN, flags [I] (0x08), vni 10010
IP 172.16.10.21 > 172.16.10.12: ICMP echo request, id 244, seq 0, length 64
11:23:16.853485 IP 10.255.1.11.248 > 10.255.1.12.4789: VXLAN, flags [I] (0x08), vni 10010
IP 172.16.10.21 > 172.16.10.12: ICMP echo request, id 244, seq 0, length 64
! output trimmed

host-2’s MAC had aged out on LS-3, and its Type 2 route was withdrawn with it. LS-1 learns remote MACs from EVPN only (show interfaces Vxlan1 says “Remote MAC learning via EVPN”), so until LS-3 relearned the MAC and advertised a new Type 2, host-2 was an unknown unicast destination, the U in BUM, and LS-1 flooded. From the next packet on, the frames went to 10.255.1.13 only. The same aging explains why a show bgp evpn route-type mac-ip taken after a few idle minutes can come back empty.

In short: VXLAN added exactly 50 bytes. Broadcasts and unknown unicast leave the ingress leaf once per VTEP on the flood list; known unicast goes to one VTEP. With MAC-only Type 2 routes, every ARP floods.

Does Traffic Use Both Spines?

Between LS-1 and LS-3, every VXLAN packet has the same outer source and destination IP. Part 2 explained why that doesn’t defeat ECMP: the VTEP sets the outer UDP source port from a hash of the inner headers, so different host conversations get different ports and spread across both spines. Part 2 also left the question open for cEOS.

The test gives host-1 eight extra addresses, 172.16.10.21 to .28, and pings host-2 twice from each, while tcpdump watches both of LS-1’s uplinks. Each address is a separate inner flow. The script is scripts/ecmp-test.sh in the repo.

The first half of the answer is yes. Every flow got its own source port, kept it for both pings, and used the same port in both directions:

host-1 sourceOuter UDP source port
172.16.10.21248
172.16.10.221016
172.16.10.23760
172.16.10.243576
172.16.10.253320
172.16.10.264088
172.16.10.273832
172.16.10.282552

Most of these sit well below the 49152 to 65535 range RFC 7348 recommends. That’s a recommendation, not a requirement, and the receiving VTEP only cares about the destination port.

The second half is no. All eight flows, in both directions, crossed SP-1. The capture on Ethernet2, toward SP-2, stayed empty:

TCPDUMP
### LS-1 et2 (Ethernet2) udp port 4789
reading from file outputs/20261015-1123_LS-1_et2_ecmp.pcap, link-type EN10MB (Ethernet), snapshot length 262144

EOS has both paths. The Linux routing table inside LS-1’s container has one:

EOS
LS-1#show ip route 10.255.1.13
 B E      10.255.1.13/32 [200/0]
           via 10.1.11.0, Ethernet1
           via 10.2.11.0, Ethernet2
! output trimmed
SHELL
sudo ip netns exec clab-l2-overlay-LS-1 ip route show 10.255.1.13
10.255.1.13 via 10.1.11.0 dev et1 proto gated

That looked like the whole explanation, and it isn’t. Ivan Pepelnjak tested ECMP on cEOS 4.35.2F and saw the same single next hop in the Linux table, yet traffic forwarded through the switch was spread across both paths. Traffic the switch originated itself used one path. A VTEP builds the outer VXLAN packet itself, so from cEOS’s point of view encapsulated traffic may count as locally originated. That fits everything captured here, including the detail that both leaves always picked Ethernet1, the first and only next hop in their kernel tables. It isn’t proven, and this fabric has no plain transit-ECMP case to compare against: every packet a leaf sends into the fabric is VXLAN, and each spine has a single path to each leaf.

The same single kernel path exists for Part 1’s Loopback0 routes. Part 1 showed ECMP in the routing table, which is accurate as far as it goes; whether the lab image would spread forwarded traffic across both spines is a separate question.

In short: the VXLAN source port did its job, one port per flow. cEOS in containerlab still sent every VXLAN flow over one uplink. That’s a property of the lab image, not of EVPN; hardware VTEPs hash in the forwarding chip.

Lab Summary

Each step had a prediction written down before it ran. Two were wrong, and one finding wasn’t predicted at all; all three are marked.

StepExpectedObserved
MTUFabric links at 1500, no room for VXLAN1500. netlab rejects 9214 for EOS; 9194 set on switches only, 9000-byte DF ping works
Loopback1, Part 1 policyRoute in LS-1’s table, not advertisedAs expected; then dropped by SP-1 on arrival (2 received, 1 accepted) until all four filters allowed the VTEP range
EVPN sessionsEstablished, no prefixesAs expected; EVPN only, thanks to netlab’s no bgp default ipv4-unicast
No extended communitiesRoutes arrive without RT or encapsulation, nothing importedAs expected: valid and best, 0 remote VTEPs, ping fails, sessions up
Spine next hop without next-hop-unchangedRewritten to the spine’s Loopback0Wrong: next hop stayed the leaf’s VTEP; adding the command changed nothing
With extended communitiesFlood list built, MACs on Vx1, ping worksAs expected; ping TTL 64, Type 2 routes MAC only
Automatic RT, own ASN onlyFails silentlyAs expected: 65103:268445466 (RFC 8365 encoding), not imported elsewhere; any-as:268445466 fixes it
EVPN transit through leavesNot predictedLeaves passed spine-learned routes to the other spine; ^$ filter stops it
ARPFirst ARP floods, second floods too (no IP in Type 2)As expected: both requests reached host-2
ECMP across spinesDifferent flows on different spinesWrong: one source port per flow, but every flow used Ethernet1 to SP-1
Rebuild from templatesFull overlay from netlab up aloneAs expected; scripts/verify-overlay.sh passes

What’s Next

host-1 and host-2 now share a broadcast domain across a routed fabric, and nothing on the leaves knows an IP address. Part 4 changes that. Each leaf becomes the anycast gateway for its subnets, a second subnet appears on LS-3, and routing between them uses symmetric IRB with a per-tenant L3 VNI. The Type 2 routes start carrying IP addresses, Type 5 routes carry subnets, and the ARP suppression this post couldn’t show gets its chance. The traceroute should show two routing hops where this post’s ping showed none.

References

Recent Posts