Declare MLAG / vPC peer groups
When you need this
Section titled “When you need this”- “sw-core-1 and sw-core-2 are vPC peers, but every trace shows duplicate entries through both — that’s not how the real path looks.”
- “My audit dashboard has 200 ✓ peer-link rows for every real one-way-cable alert. I can’t see the signal.”
- “We added a third pair to the fabric this morning and the traceroute query just got slower.”
l2trace’s L2 traceroute and adjacency audit are MLAG-aware once you
declare the pair. Before declaration: the walker treats peer-link
adjacencies as real hops, audits show peer-link rows as ✓ healthy,
and the same MAC seen on both peers reads as a constant “MAC move.”
After: the trace walks one logical path, the audit hides peer-links
by default, and disagreement detection works correctly per-peer.
The mechanism is documented in How MLAG-collapsed traceroute works. This page is the operator workflow.
Three commands
Section titled “Three commands”Declare a peer pair
Section titled “Declare a peer pair”docker compose run --rm reconciler l2trace mlag create \ --hosts sw-core-1,sw-core-2Output:
grouped sw-core-1, sw-core-2 into mlag_group_id=1Effective immediately — the next l2trace trace and the next AUDIT
refresh in the TUI use the new grouping. No reconciler restart
needed.
Idempotent: re-running with the same pair is a no-op. Adding a third host to an existing pair promotes the third into the existing group:
l2trace mlag create --hosts sw-core-1,sw-core-3# Output: grouped sw-core-1, sw-core-3 into mlag_group_id=1# (sw-core-1's existing group ID is reused; sw-core-3 joins it)List declared groups
Section titled “List declared groups”docker compose run --rm reconciler l2trace mlag list MLAG groups┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓┃ group_id ┃ hosts ┃┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩│ 1 │ sw-core-1, sw-core-2 ││ 2 │ sw-edge-a, sw-edge-b │└──────────┴──────────────────────────┘Group IDs are auto-assigned and opaque to operators — you don’t manage them by number, you manage them by hostnames.
Dissolve a group
Section titled “Dissolve a group”docker compose run --rm reconciler l2trace mlag dissolve \ --hosts sw-core-1,sw-core-2Resets mlag_group_id = NULL on the listed hosts. Use after physical
MLAG teardown (replacing the peer-link with a routed uplink, splitting
the pair into independent switches, etc).
Silent no-op on hostnames that aren’t in any group — useful when a
device remove already cleaned up.
Conflict handling
Section titled “Conflict handling”Mixing hosts that already belong to different non-NULL groups is a hard error:
$ l2trace mlag create --hosts sw-core-1,sw-edge-ahosts already belong to conflicting MLAG groups: [1, 2]. Dissolve first.This prevents accidentally merging two separate peer-pairs into a single quad-group. If you really do want a quad, dissolve one pair first, then re-create with all four:
l2trace mlag dissolve --hosts sw-edge-a,sw-edge-bl2trace mlag create --hosts sw-core-1,sw-core-2,sw-edge-a,sw-edge-bWhat changes after declaration
Section titled “What changes after declaration”Trace output
Section titled “Trace output”After declaring the MLAG pair (and seeding a fabric via
make seed-realistic, which now includes an MLAG-paired core), the
trace correctly walks one peer without detouring through the
peer-link. Same shape in the TUI’s TRACE screen:
And from the CLI:
$ l2trace trace --src 00:03:93:44:20:82 --dst 00:50:56:78:9b:34 --vlan 10
L2 path 00:03:93:44:20:82 → 00:50:56:78:9b:34 vlan=10 as_of=2026-05-11T22:29:56.135884+00:00┏━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━┳━━━━━━┓┃ hop ┃ device ┃ in port ┃ out port ┃ role ┃ via ┃┡━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━╇━━━━━━┩│ 0 │ sw-access-1 │ Eth3 │ Eth8 │ trunk │ gnmi ││ 1 │ sw-core-1 (mlag-1) │ Eth1 │ Eth2 │ trunk │ gnmi ││ 2 │ sw-access-2 │ Eth8 │ Eth4 │ access │ gnmi │└─────┴────────────────────┴─────────┴──────────┴────────┴──────┘termination: reachedThe (mlag-1) annotation tells the operator that sw-core-1 is one
half of a paired logical node — under failover the same flow would
be carried by sw-core-2. Without MLAG declaration, the trace
would still have walked through sw-core-1 in this seeded scenario
(the peer-link adjacency between sw-core-1 and sw-core-2 doesn’t
participate in the CAM-following walk), but the renderer wouldn’t
show the suffix and the audit (below) would be polluted with
peer-link rows.
Audit output
Section titled “Audit output”Without --include-peer-links (default), peer-link adjacencies
are filtered:
$ l2trace audit-adjacencies --show-healthy
adjacency audit┏━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━┳━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┓┃ status ┃ local ┃ port ┃ → ┃ peer ┃ peer port ┃ source ┃┡━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━╇━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━┩│ ✓ │ sw-access-1 │ Eth8 │ → │ sw-core-1 │ Eth1 │ gnmi ││ ✓ │ sw-access-2 │ Eth8 │ → │ sw-core-1 │ Eth2 │ gnmi ││ ✓ │ sw-core-1 │ Eth1 │ → │ sw-access-1 │ Eth8 │ gnmi ││ ✓ │ sw-core-1 │ Eth2 │ → │ sw-access-2 │ Eth8 │ gnmi │└────────┴─────────────┴──────┴───┴─────────────┴───────────┴────────┘Four rows: the two real uplinks audited in both directions.
With --include-peer-links, the peer-link adjacency rows
(sw-core-1.Eth-pl ↔ sw-core-2.Eth-pl) come back into view:
$ l2trace audit-adjacencies --show-healthy --include-peer-links
adjacency audit┏━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┓┃ status ┃ local ┃ port ┃ → ┃ peer ┃ peer port ┃ source ┃┡━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━┩│ ✓ │ sw-access-1 │ Eth8 │ → │ sw-core-1 │ Eth1 │ gnmi ││ ✓ │ sw-access-2 │ Eth8 │ → │ sw-core-1 │ Eth2 │ gnmi ││ ✓ │ sw-core-1 │ Eth1 │ → │ sw-access-1 │ Eth8 │ gnmi ││ ✓ │ sw-core-1 │ Eth2 │ → │ sw-access-2 │ Eth8 │ gnmi ││ ✓ │ sw-core-1 │ Eth-pl │ → │ sw-core-2 │ Eth-pl │ gnmi ││ ✓ │ sw-core-2 │ Eth-pl │ → │ sw-core-1 │ Eth-pl │ gnmi │└────────┴─────────────┴────────┴───┴─────────────┴───────────┴────────┘Both peer-link rows show as ✓ healthy — they’re each other’s reverse. That’s expected (peer-sync depends on it), which is why the default view hides them: they’d always be noise in the “what’s actually broken?” picture.
l2trace mlag list
Section titled “l2trace mlag list”Real output from the seeded MLAG group:
$ l2trace mlag list
MLAG groups┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓┃ group_id ┃ hosts ┃┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩│ 1 │ sw-core-1, sw-core-2 │└──────────┴──────────────────────┘Cross-source disagreement detection
Section titled “Cross-source disagreement detection”Unchanged. Each peer device still tracks its OWN gNMI/SNMP/SSH sources independently — l2trace doesn’t merge their telemetry. So a real disagreement on either peer (gNMI says one port, SNMP says another, on the same device) still flags via the OPS screen’s disagreement pane. MLAG declaration is about topology semantics, not about telemetry semantics.
Bitemporal aside
Section titled “Bitemporal aside”l2trace mlag create is NOT bitemporal — there’s no “what was the
MLAG declaration at 14:42 last Tuesday?” query. The grouping is a
single nullable column on device, treated like vendor/model: it
reflects the current operational reality.
If your MLAG configuration changes (a pair is torn down, a new pair
is added), the change takes effect immediately for new traces. The
bitemporal log of adjacency and mac_observation is unaffected —
those keep their historical truth; only the trace’s interpretation
of that history changes.
See also
Section titled “See also”- How MLAG-collapsed traceroute works — the design rationale + algorithm
- Audit LLDP adjacencies — how the peer-link filter interacts with the audit
- CLI reference — full flag matrix for the
mlagsubcommand group