A hands-on lab for learning Segment Routing over IPv6 (SRv6) with FRRouting (FRR) inside Docker containers. It builds a four-node network running IS-IS with SRv6 extensions, letting you observe SRv6 SID distribution, default shortest-path routing, and explicit source routing with the Segment Routing Header (SRH) — all without physical hardware. The setup is fully automated: build one Docker image, bring up the containers, and IS-IS converges with SRv6 SIDs on its own.
For background on SRv6 architecture, SID structure, endpoint behaviors, and compressed SIDs, see the SRv6 primer. For MPLS and SR-MPLS basics, see the MPLS primer.
-
Docker Engine (v20.10 or later) with the
docker composeplugin. -
Linux host kernel with SRv6 support. Docker containers share the host kernel, and FRR is control plane only — it computes routes and distributes SRv6 SIDs, but the Linux kernel does the actual packet forwarding. The SRv6 data plane requires these kernel options to be enabled:
Kernel option What it does CONFIG_LWTUNNELLightweight tunnels — required for all encaproute typesCONFIG_IPV6_SEG6_LWTUNNELSRv6 lightweight tunnel support — encap seg6andseg6localactionsMost desktop and server Linux distributions (Ubuntu, Fedora, Arch) ship kernels with these options enabled. WSL2's default kernel does not include them — you will need a custom WSL2 kernel compiled with the options above. Without them, the IS-IS control plane still works (adjacencies form, SRv6 SIDs are distributed) but the SRv6 source-routing experiment in the data plane will fail.
To check your kernel:
# WSL2 (config is in /proc) zcat /proc/config.gz 2>/dev/null | grep -E "CONFIG_LWTUNNEL|CONFIG_IPV6_SEG6_LWTUNNEL" # Ubuntu / standard Linux VM (config is in /boot) grep -E "CONFIG_LWTUNNEL|CONFIG_IPV6_SEG6_LWTUNNEL" /boot/config-$(uname -r)
Both should show
=yor=m.Which kernel version do I need? This lab uses conventional (full-length) SRv6 SIDs — the
EndandEnd.Xbehaviors defined in RFC 8986. The Linux kernel has supported these since kernel 4.10 (February 2017), so any modern distribution works. Ubuntu 22.04 LTS (kernel 5.15) is recommended — it is widely available and well-tested with this lab.Ubuntu LTS Default (GA) kernel Conventional SRv6 SIDs SRv6 uSID (micro-SID) 20.04 5.4 ✅ Supported ❌ 22.04 5.15 ✅ Supported ❌ 22.04.5 (HWE kernel) 6.8 ✅ Supported ✅ Full support 24.04 6.8 ✅ Supported ✅ Full support If you later want to experiment with SRv6 uSID (micro-SID / Compressed SID) — the compact encoding that packs multiple micro-instructions into a single 128-bit SID address — you will need a newer kernel. uSID behaviors were added incrementally to mainline Linux: uN (uSID Endpoint) in kernel 6.1 (Dec 2022) and uA (uSID Adjacency) in kernel 6.6 (Oct 2023). Use Ubuntu 24.04 LTS (ships kernel 6.8, supports everything) or install the HWE kernel on Ubuntu 22.04 (
sudo apt install linux-generic-hwe-22.04, which upgrades to 6.8). On the FRR side, uSID locators are supported since FRR 8.1 via thebehavior usidcommand under an SRv6 locator. -
Bridge netfilter disabled. Docker's default bridge networking passes IPv6 packets through
ip6tables, which can silently drop packets with an SRv6 Routing Header. Before running the SRv6 experiment, disable bridge netfilter:sudo sysctl -w net.bridge.bridge-nf-call-ip6tables=0
Without this, the IS-IS control plane works fine (it runs on Layer 2, bypassing the bridge filter), but SRv6 data-plane packets between containers will be dropped.
-
Basic familiarity with the Linux command line.
-
No physical routers or switches are needed.
FRRouting (FRR) is the open-source routing suite used in this lab. FRR supports SRv6 through two IGP options:
| IGP | SRv6 support | Maturity in FRR |
|---|---|---|
| IS-IS | Full — locators, automatic SID allocation, End/End.X behaviors | Production-ready (since FRR 7.5+) |
| OSPFv3 | Partial — basic SRv6 extensions | Experimental, fewer features |
The previous labs (FRR-LabNet, BFD-LabNet) used OSPF because it was the right tool for learning basic routing and BFD. For SRv6, this lab switches to IS-IS for three reasons:
-
FRR maturity. FRR's SRv6 implementation is built on IS-IS. The IS-IS SRv6 code path is well-tested and feature-complete; OSPFv3 SRv6 is still experimental.
-
Real-world alignment. The major SRv6 deployments — SoftBank, China Mobile, hyperscaler AI fabrics — all run IS-IS. Learning IS-IS + SRv6 together reflects how production networks actually work.
-
Protocol fit. IS-IS runs directly on Layer 2 (it does not need IP to bootstrap), and its TLV-based encoding makes it straightforward to add new extensions like SRv6 locators. OSPF's LSA structure is less flexible for carrying arbitrary SRv6 data.
If you are new to IS-IS: it is a link-state routing protocol, just like OSPF. Every router floods a description of its links to every other router, all routers build the same topology map, and each one independently runs the shortest-path algorithm. The concepts you learned with OSPF — neighbors, adjacencies, convergence, link-state database — all apply. The main visible differences are the addressing (IS-IS uses ISO NET addresses instead of router IDs) and the terminology (IS-IS calls its hello packets IIH, and its link-state entries LSPs rather than LSAs).
Two IS-IS specifics appear in this lab's configuration:
-
NET (Network Entity Title). A NET is IS-IS's equivalent of a router ID — an ISO address in the format
<area>.<system-ID>.<selector>. In this lab, H1's NET is49.0001.0000.0000.0001.00, which breaks down as:49.0001— the IS-IS area (area 49.0001, shared by all four routers)0000.0000.0001— the system ID (unique per router —...0001for H1,...0002for H2, etc.)00— the selector (always00for a NET, meaning "the router itself")
-
IS-IS levels. IS-IS defines two routing levels: Level 1 (intra-area, similar to OSPF intra-area routes) and Level 2 (inter-area, similar to OSPF backbone routes). A single router can be Level-1-only, Level-2-only, or both. This lab uses
is-type level-1because all four routers are in the same area and inter-area routing is not needed.
Each router is configured with an SRv6 locator — a /48 IPv6 prefix that the router owns. For example, H1's locator is fcbb:bb00:1::/48. The locator is advertised by IS-IS, so every router in the network can route toward it.
FRR automatically allocates End SIDs from each router's locator. An End SID is the SRv6 equivalent of a Node SID: it says "forward this packet to me, then process the next segment." When IS-IS converges, every router knows every other router's End SID — this is what makes SRv6 source routing possible.
You do not configure SIDs by hand. FRR allocates them, IS-IS distributes them, and you discover them with show isis segment-routing srv6 node.
The lab creates four containers in a diamond topology connected by four Docker bridge networks:
| Container | Networks | IPv6 Addresses | Loopback | SRv6 Locator |
|---|---|---|---|---|
| H1 | link_h1_h2, link_h1_h3 | fd00:12::1, fd00:13::1 | fc00::1/128 | fcbb:bb00:1::/48 |
| H2 | link_h1_h2, link_h2_h4 | fd00:12::2, fd00:24::2 | fc00::2/128 | fcbb:bb00:2::/48 |
| H3 | link_h1_h3, link_h3_h4 | fd00:13::3, fd00:34::3 | fc00::3/128 | fcbb:bb00:3::/48 |
| H4 | link_h2_h4, link_h3_h4 | fd00:24::4, fd00:34::4 | fc00::4/128 | fcbb:bb00:4::/48 |
IS-IS link metrics: All links use the default metric of 10, except the H1–H3 link which is set to 20 on both sides. This makes the default shortest path from H1 to H4 always go through H2 (total cost 20) rather than H3 (total cost 30). The SRv6 experiment then overrides this by source-routing traffic through H3.
Note on interface names: Docker Compose does not guarantee the order of interface names (
eth0,eth1) inside containers. The entrypoint script detects each interface by its IPv6 address and generates the correct IS-IS configuration at startup. Useip -6 addr showinside any container to see the actual mapping.
Why a diamond? Two parallel paths between H1 and H4 create a clear contrast: default routing picks one path, SRv6 lets you pick the other. The topology also supports future experiments — TI-LFA (break a link, observe the backup segment list), SRv6 Policy, and BGP L3VPN over SRv6 (add VRFs on the edge nodes).
Build the Docker image:
docker build --tag frr-srv6 docker/Start the containers:
docker compose -f docker/docker-compose.yml up -dIS-IS will converge and SRv6 SIDs will be distributed within a few seconds. Jump to Verify the Lab to confirm.
The entrypoint script (docker/entrypoint.sh) runs inside each container at startup:
- Enables IPv6 forwarding (
net.ipv6.conf.all.forwarding=1). - Enables the SRv6 data plane (
net.ipv6.conf.*.seg6_enabled=1) on all interfaces. This kernel setting allows the interface to receive and process SRv6 packets — specifically,seg6localactions like End and End.X that operate on the Segment Routing Header. - Detects which interface carries which subnet (Docker interface naming is non-deterministic).
- Generates the FRR configuration with the correct interface names and IS-IS metrics.
- Starts FRR, which launches
zebra(the route manager that installs routes into the Linux kernel) andisisd(the IS-IS daemon). - Configures the SRv6 locator via
vtyshafter FRR is running.vtyshis FRR's unified command-line shell — similar to a Cisco IOS CLI — that lets you query and configure all FRR daemons from a single interface.
The configuration files live under docker/configs/:
docker/configs/
├── daemons # shared — enables zebra and isisd
├── frr-H1/frr.conf # reference: loopback fc00::1, IS-IS, SRv6 locator
├── frr-H2/frr.conf # reference: loopback fc00::2, IS-IS, SRv6 locator
├── frr-H3/frr.conf # reference: loopback fc00::3, IS-IS, SRv6 locator
└── frr-H4/frr.conf # reference: loopback fc00::4, IS-IS, SRv6 locator
The daemons file is the only configuration applied directly — it tells FRR which daemons to start (zebra and isisd). The per-node frr.conf files are reference copies that show the expected final configuration for each router; the entrypoint script generates the actual /etc/frr/frr.conf dynamically at startup because interface names are not known until the container is running.
Edit the entrypoint script or the reference frr.conf files, rebuild the image, and recreate the containers:
docker build --tag frr-srv6 docker/
docker compose -f docker/docker-compose.yml up -d --force-recreateYou can also make live changes with vtysh (e.g., docker exec -it H1 vtysh), but those changes are lost when the container restarts.
Enter H1 and confirm IS-IS neighbors are up:
docker exec H1 vtysh -c "show isis neighbor"H1 should show two adjacencies — H2 and H3:
Area SRv6LAB:
System Id Interface L State Holdtime SNPA
H2 eth0 1 Up 29 ...
H3 eth1 1 Up 29 ...
The interface names above (
eth0,eth1) are examples. Your container may assign them in a different order. What matters is that both H2 and H3 appear with stateUp.
View the SRv6 SIDs that IS-IS has distributed across all nodes:
docker exec H1 vtysh -c "show isis segment-routing srv6 node"Each node should appear with its SRv6 capabilities. View the IS-IS routes to see the SRv6 locators and auto-allocated End.X SIDs:
docker exec H1 vtysh -c "show ipv6 route isis"You should see routes to each node's locator (e.g., fcbb:bb00:2::/48, fcbb:bb00:3::/48) and local seg6local End.X entries for this node's own SIDs.
You can also view the local SID table:
docker exec H1 vtysh -c "show segment-routing srv6 locator"Verify that H1 reaches H4 via the shortest path (through H2):
docker exec H1 traceroute6 -s fc00::1 fc00::4Expected output shows two hops — H2 then H4:
1 fd00:12::2 (fd00:12::2) ... (H2)
2 fc00::4 (fc00::4) ... (H4)
The path goes H1 → H2 → H4 (cost 20), not through H3 (cost 30).
docker exec H1 ping -6 -c 3 -I fc00::1 fc00::4All three pings should succeed, confirming the full routing path through IS-IS.
Kernel requirement: This experiment uses
ip route encap seg6, which requiresCONFIG_LWTUNNELandCONFIG_IPV6_SEG6_LWTUNNELin the host kernel. See Prerequisites. If your kernel lacks these options, you can still observe the IS-IS control plane (SID distribution, route computation) but cannot perform data-plane source routing.
This experiment overrides the default shortest path. Instead of H1 → H2 → H4 (the IS-IS default), you force the packet through H3 using an SRv6 segment list.
If you have not already done so in Check Default Routing, verify that H1's path to H4 goes through H2:
docker exec H1 traceroute6 -s fc00::1 fc00::4The first hop is H2 (fd00:12::2). This is IS-IS choosing the lowest-cost path.
FRR automatically allocates End.X SIDs (cross-connect SIDs that forward to a specific adjacency). However, End.X SIDs use link-local next-hops internally, and older kernels (including 5.15) have a known limitation where the seg6local input route lookup cannot resolve a link-local next-hop across a different interface than the one the packet arrived on.
Instead, we create a manual End SID on H3. The End behavior updates the IPv6 Destination Address from the SRH and then performs a standard route lookup — which works reliably on all kernel versions.
First, find the interface on H3 that faces H1 (the fd00:13:: subnet):
docker exec H3 ip -o -6 addr show | grep fd00:13Use the interface name from the output (shown as eth1 below — yours may differ) in the following command:
docker exec H3 ip -6 route add fcbb:bb00:3:100::/128 \
encap seg6local action End dev eth1The
devparameter must be the interface on which the SRv6 packet will arrive (i.e., the interface facing H1).
Replace the default route to H4's loopback with an SRv6-encapsulated route that visits H3 first:
docker exec H1 ip -6 route replace fc00::4/128 \
encap seg6 mode inline segs fcbb:bb00:3:100:: via fd00:13::3This tells the Linux kernel: "For packets to fc00::4, insert a Segment Routing Header with H3's End SID, and send via H3's directly connected address."
The command uses mode inline, which inserts the SRH directly into the original packet rather than wrapping it in an outer IPv6 header. The alternative, mode encap, would add a new outer IPv6 header around the original packet (IPv6-in-IPv6 encapsulation). Inline mode is simpler for this experiment because it avoids the overhead of an extra 40-byte header — the original packet is modified in place:
- The Destination Address is overwritten with H3's End SID (
fcbb:bb00:3:100::) - The SRH stores the original destination (
fc00::4) so it can be restored at H3 - The original packet is not encapsulated — there is no outer header
docker exec H1 traceroute6 -s fc00::1 fc00::4The first hop is now H3 (fd00:13::3) instead of H2. The packet takes the longer path because you told it to:
1 fd00:13::3 (fd00:13::3) ... (H3 — forced by SRv6)
2 fc00::4 (fc00::4) ... (H4)
docker exec H1 ping -6 -c 3 -I fc00::1 fc00::4All pings should succeed, confirming end-to-end delivery through the SRv6-dictated path.
Capture SRv6 packets arriving at H3. First, find the interface on H3 that faces H1 (the same interface used in Step 2):
docker exec H3 ip -o -6 addr show | grep fd00:13Use that interface name (shown as eth1 below) to capture packets with a Routing Header (protocol 43):
docker exec H3 tcpdump -i eth1 -n -vv -c 2 "ip6 proto 43"In a second terminal, send pings:
docker exec H1 ping -6 -c 2 -I fc00::1 fc00::4You should see the Segment Routing Header in the capture:
fc00::1 > fcbb:bb00:3:100:: : srcrt (len=4, type=4, segleft=1)
You can also capture the outbound side on H3 (toward H4) to see the packet after End processing. Find the interface facing H4:
docker exec H3 ip -o -6 addr show | grep fd00:34Then capture on that interface (shown as eth0 below):
docker exec H3 tcpdump -i eth0 -n -vv -c 2 "host fc00::4"After End SID processing, the packet's Destination Address is restored to fc00::4 and segments_left is decremented to 0:
fc00::1 > fc00::4 : srcrt (len=4, type=4, segleft=0)
This is the SRH from the SRv6 primer's packet walk made real.
To inspect the packets visually, you can re-run the capture with -w to save a pcap file (e.g., tcpdump -i eth1 -n -w /tmp/srh.pcap -c 5 "ip6") and open it in Wireshark. The dissection shows the full SRH structure inside each packet:
The dissection confirms every field from the primer:
- IPv6 header: Source
fc00::1(H1), Destinationfcbb:bb00:3:100::(H3's End SID — the active segment). Next Header is 43 (Routing Header for IPv6). - Routing Header (Segment Routing): Type 4 (SRv6), Segments Left = 1 (one segment still to process), Last Entry = 1 (two entries in the list, indexed 0 and 1). The segment list is stored in reverse order —
Address[0]: fc00::4is the final destination andAddress[1]: fcbb:bb00:3:100::is the first segment (H3's End SID). - ICMPv6 Echo Request: The original ping payload, carried after the SRH.
Restore default IS-IS routing:
docker exec H1 ip -6 route del fc00::4/128
docker exec H3 ip -6 route del fcbb:bb00:3:100::/128Verify the path reverts to H2:
docker exec H1 traceroute6 -s fc00::1 fc00::4| State | Path H1 → H4 | Why |
|---|---|---|
| Before (IS-IS default) | H1 → H2 → H4 | IS-IS shortest path (cost 20) |
| During (SRv6 source route) | H1 → H3 → H4 | Segment list forces the packet through H3's End SID |
| After (route removed) | H1 → H2 → H4 | IS-IS default restored |
The IS-IS control plane never changed. The SRv6 source route on H1 overrode the forwarding decision for that one prefix, exactly as described in the primer: the path lives in the packet, not in the routers.
| Symptom | Cause | Fix |
|---|---|---|
ip route encap seg6 fails with "CONFIG_LWTUNNEL is not enabled" |
Host kernel lacks SRv6 support | Use a kernel compiled with CONFIG_LWTUNNEL=y and CONFIG_IPV6_SEG6_LWTUNNEL=y. WSL2's default kernel does not include these. |
| SRv6 ping 100% loss but IS-IS works | Docker bridge netfilter drops SRH packets | Run sudo sysctl -w net.bridge.bridge-nf-call-ip6tables=0 on the host |
| End.X SID returns "Address unreachable" | Kernel 5.15 cannot resolve End.X link-local nexthop from a different ingress interface | Use a manual End SID instead of the auto-allocated End.X (see the experiment steps above) |
| Interface names differ from examples | Docker Compose does not guarantee interface ordering | Use ip -o -6 addr show inside the container to find the correct interface for each subnet |
docker compose -f docker/docker-compose.yml down
