[RFC PATCH net-next 3/3] selftests: seg6: add SRv6 srl2 + End.DT2U L2 VPN test

From: Andrea Mayer

Date: Sat Mar 21 2026 - 20:08:34 EST


Add a selftest for the srl2 Ethernet pseudowire device exercising the
full L2 VPN data path: srl2 for encapsulation and End.DT2U for
decapsulation, connected through a Linux bridge.

The test verifies IPv4/IPv6 host-to-host and host-to-gateway
connectivity over a two-router topology.

Cc: Shuah Khan <shuah@xxxxxxxxxx>
Cc: linux-kselftest@xxxxxxxxxxxxxxx
Signed-off-by: Andrea Mayer <andrea.mayer@xxxxxxxxxxx>
---
tools/testing/selftests/net/Makefile | 1 +
tools/testing/selftests/net/config | 1 +
.../selftests/net/srv6_srl2_l2vpn_test.sh | 621 ++++++++++++++++++
3 files changed, 623 insertions(+)
create mode 100755 tools/testing/selftests/net/srv6_srl2_l2vpn_test.sh

diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 6bced3ed798b..d2301387b21c 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -92,6 +92,7 @@ TEST_PROGS := \
srv6_end_x_next_csid_l3vpn_test.sh \
srv6_hencap_red_l3vpn_test.sh \
srv6_hl2encap_red_l2vpn_test.sh \
+ srv6_srl2_l2vpn_test.sh \
stress_reuseport_listen.sh \
tcp_fastopen_backup_key.sh \
test_bpf.sh \
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index 2a390cae41bf..77d5c7941969 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -48,6 +48,7 @@ CONFIG_IPV6_ROUTER_PREF=y
CONFIG_IPV6_RPL_LWTUNNEL=y
CONFIG_IPV6_SEG6_LWTUNNEL=y
CONFIG_IPV6_SIT=y
+CONFIG_IPV6_SRL2=m
CONFIG_IPV6_VTI=y
CONFIG_IPVLAN=m
CONFIG_IPVTAP=m
diff --git a/tools/testing/selftests/net/srv6_srl2_l2vpn_test.sh b/tools/testing/selftests/net/srv6_srl2_l2vpn_test.sh
new file mode 100755
index 000000000000..eefc1274e139
--- /dev/null
+++ b/tools/testing/selftests/net/srv6_srl2_l2vpn_test.sh
@@ -0,0 +1,621 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# author: Andrea Mayer <andrea.mayer@xxxxxxxxxxx>
+#
+# This script tests the full SRv6 L2 VPN data path using the srl2
+# virtual Ethernet device for L2 frame encapsulation and the End.DT2U
+# behavior (RFC 8986, Section 4.11) for decapsulation.
+#
+# This test exercises the full SRv6 L2 VPN data path using the srl2
+# device as the TX-side encapsulator. Each SRv6 router uses a bridge
+# (br0) with two ports:
+# - veth-hs: connects to the host
+# - srl2-0: SRv6 L2 tunnel device for encap/decap
+#
+# TX path: the host sends an L2 frame via veth-hs. The bridge forwards
+# the frame to srl2-0 (L2 forwarding based on dst MAC). srl2-0
+# encapsulates the frame in IPv6+SRH and transmits.
+#
+# RX path: an SRv6 packet arrives carrying an inner Ethernet frame.
+# End.DT2U decapsulates and delivers the frame on srl2-0 via
+# netif_rx(). Since srl2-0 is a bridge port, the bridge performs MAC
+# learning and L2 forwarding to deliver the frame to veth-hs.
+#
+# Note: no static MAC addresses or neighbor entries are needed here.
+# ARP works naturally through the
+# bridge and the SRv6 tunnel: ARP requests are broadcast, flooded by
+# the bridge to srl2-0, encapsulated, decapsulated by End.DT2U on the
+# remote side, and flooded to the destination host.
+#
+# Topology:
+#
+# cafe::1 cafe::2
+# 10.0.0.1 10.0.0.2
+# +--------+ +--------+
+# | | | |
+# | hs-1 | | hs-2 |
+# | | | |
+# +---+----+ +----+---+
+# cafe::/64 | | cafe::/64
+# 10.0.0.0/24 | | 10.0.0.0/24
+# +-----+------+ +------+-----+
+# | veth-hs | | veth-hs |
+# | | | fcf0:0:1:2::/64 | | |
+# | br0 +-------------------------+ br0 |
+# | | | | | |
+# | srl2-0 | | srl2-0 |
+# | rt-1 | | rt-2 |
+# +------------+ +------------+
+#
+#
+# Every fcf0:0:x:y::/64 network interconnects the SRv6 routers rt-x with
+# rt-y in the IPv6 operator network.
+#
+# Local SID table
+# ===============
+#
+# Each SRv6 router is configured with a Local SID table in which SIDs are
+# stored. Considering the given SRv6 router rt-x, the following SID is
+# configured in the Local SID table:
+#
+# Local SID table for SRv6 router rt-x
+# +-----------------------------------------------------------+
+# |fcff:x::d20 is associated with the SRv6 End.DT2U behavior |
+# +-----------------------------------------------------------+
+#
+# SRv6 L2 encapsulation
+# =====================
+#
+# Each router's srl2-0 device is configured with a SID list pointing to
+# the remote router's End.DT2U SID:
+#
+# rt-1 srl2-0: segs fcff:2::d20 (encap towards rt-2)
+# rt-2 srl2-0: segs fcff:1::d20 (encap towards rt-1)
+#
+# Each SID list consists of only one SID. The srl2 device encapsulates
+# the L2 frame in an outer IPv6 header with an SRH containing the
+# segment list.
+#
+
+source lib.sh
+
+readonly DUMMY_DEVNAME="dum0"
+readonly SRL2_DEVNAME="srl2-0"
+readonly RT2HS_DEVNAME="veth-hs"
+readonly BRIDGE_DEVNAME="br0"
+readonly HS_VETH_NAME="veth0"
+readonly LOCALSID_TABLE_ID=90
+readonly IPv6_RT_NETWORK=fcf0:0
+readonly IPv6_HS_NETWORK=cafe
+readonly IPv4_HS_NETWORK=10.0.0
+readonly VPN_LOCATOR_SERVICE=fcff
+readonly DT2U_FUNC=0d20
+
+PING_TIMEOUT_SEC=4
+PAUSE_ON_FAIL=${PAUSE_ON_FAIL:=no}
+
+ROUTERS=''
+HOSTS=''
+
+SETUP_ERR=1
+
+ret=${ksft_skip}
+nsuccess=0
+nfail=0
+
+log_test()
+{
+ local rc="$1"
+ local expected="$2"
+ local msg="$3"
+
+ if [ "${rc}" -eq "${expected}" ]; then
+ nsuccess=$((nsuccess+1))
+ printf "\n TEST: %-60s [ OK ]\n" "${msg}"
+ else
+ ret=1
+ nfail=$((nfail+1))
+ printf "\n TEST: %-60s [FAIL]\n" "${msg}"
+ if [ "${PAUSE_ON_FAIL}" = "yes" ]; then
+ echo
+ echo "hit enter to continue, 'q' to quit"
+ read a
+ [ "$a" = "q" ] && exit 1
+ fi
+ fi
+}
+
+print_log_test_results()
+{
+ printf "\nTests passed: %3d\n" "${nsuccess}"
+ printf "Tests failed: %3d\n" "${nfail}"
+
+ if [ "${ret}" -ne 1 ]; then
+ ret=0
+ fi
+}
+
+log_section()
+{
+ echo
+ echo "################################################################################"
+ echo "TEST SECTION: $*"
+ echo "################################################################################"
+}
+
+test_command_or_ksft_skip()
+{
+ local cmd="$1"
+
+ if [ ! -x "$(command -v "${cmd}")" ]; then
+ echo "SKIP: Could not run test without \"${cmd}\" tool";
+ exit "${ksft_skip}"
+ fi
+}
+
+get_rtname()
+{
+ local rtid="$1"
+
+ echo "rt_${rtid}"
+}
+
+get_hsname()
+{
+ local hsid="$1"
+
+ echo "hs_${hsid}"
+}
+
+create_router()
+{
+ local rtid="$1"
+ local nsname
+
+ nsname="$(get_rtname "${rtid}")"
+ setup_ns "${nsname}"
+}
+
+create_host()
+{
+ local hsid="$1"
+ local nsname
+
+ nsname="$(get_hsname "${hsid}")"
+ setup_ns "${nsname}"
+}
+
+cleanup()
+{
+ cleanup_all_ns
+
+ if [ "${SETUP_ERR}" -ne 0 ]; then
+ echo "SKIP: Setting up the testing environment failed"
+ exit "${ksft_skip}"
+ fi
+
+ exit "${ret}"
+}
+
+add_link_rt_pairs()
+{
+ local rt="$1"
+ local rt_neighs="$2"
+ local neigh
+ local nsname
+ local neigh_nsname
+
+ eval nsname=\${$(get_rtname "${rt}")}
+
+ for neigh in ${rt_neighs}; do
+ eval neigh_nsname=\${$(get_rtname "${neigh}")}
+
+ ip link add "veth-rt-${rt}-${neigh}" netns "${nsname}" \
+ type veth peer name "veth-rt-${neigh}-${rt}" \
+ netns "${neigh_nsname}"
+ done
+}
+
+get_network_prefix()
+{
+ local rt="$1"
+ local neigh="$2"
+ local p="${rt}"
+ local q="${neigh}"
+
+ if [ "${p}" -gt "${q}" ]; then
+ p="${q}"; q="${rt}"
+ fi
+
+ echo "${IPv6_RT_NETWORK}:${p}:${q}"
+}
+
+setup_rt_networking()
+{
+ local rt="$1"
+ local rt_neighs="$2"
+ local nsname
+ local net_prefix
+ local devname
+ local neigh
+
+ eval nsname=\${$(get_rtname "${rt}")}
+
+ for neigh in ${rt_neighs}; do
+ devname="veth-rt-${rt}-${neigh}"
+
+ net_prefix="$(get_network_prefix "${rt}" "${neigh}")"
+
+ ip -netns "${nsname}" addr \
+ add "${net_prefix}::${rt}/64" dev "${devname}" nodad
+
+ ip -netns "${nsname}" link set "${devname}" up
+ done
+
+ ip -netns "${nsname}" link add "${DUMMY_DEVNAME}" type dummy
+
+ ip -netns "${nsname}" link set "${DUMMY_DEVNAME}" up
+ ip -netns "${nsname}" link set lo up
+
+ ip netns exec "${nsname}" sysctl -wq net.ipv6.conf.all.accept_dad=0
+ ip netns exec "${nsname}" sysctl -wq net.ipv6.conf.default.accept_dad=0
+ ip netns exec "${nsname}" sysctl -wq net.ipv6.conf.all.forwarding=1
+ ip netns exec "${nsname}" sysctl -wq net.ipv4.ip_forward=1
+}
+
+setup_rt_local_sids()
+{
+ local rt="$1"
+ local rt_neighs="$2"
+ local net_prefix
+ local devname
+ local nsname
+ local neigh
+
+ eval nsname=\${$(get_rtname "${rt}")}
+
+ for neigh in ${rt_neighs}; do
+ devname="veth-rt-${rt}-${neigh}"
+
+ net_prefix="$(get_network_prefix "${rt}" "${neigh}")"
+
+ # set underlay network routes for SIDs reachability
+ ip -netns "${nsname}" -6 route \
+ add "${VPN_LOCATOR_SERVICE}:${neigh}::/32" \
+ table "${LOCALSID_TABLE_ID}" \
+ via "${net_prefix}::${neigh}" dev "${devname}"
+ done
+
+ # Local End.DT2U behavior: decapsulate L2 frames and deliver on
+ # srl2-0 which is a bridge port; the bridge then forwards to the
+ # host connected via veth-hs.
+ ip -netns "${nsname}" -6 route \
+ add "${VPN_LOCATOR_SERVICE}:${rt}::${DT2U_FUNC}" \
+ table "${LOCALSID_TABLE_ID}" \
+ encap seg6local action End.DT2U l2dev "${SRL2_DEVNAME}" \
+ dev "${DUMMY_DEVNAME}"
+
+ # all SIDs for VPNs start with a common locator. Routes and SRv6
+ # Endpoint behaviors instances are grouped together in the 'localsid'
+ # table.
+ ip -netns "${nsname}" -6 rule add \
+ to "${VPN_LOCATOR_SERVICE}::/16" \
+ lookup "${LOCALSID_TABLE_ID}" prio 999
+}
+
+setup_hs()
+{
+ local hs="$1"
+ local rt="$2"
+ local hsname
+ local rtname
+
+ eval hsname=\${$(get_hsname "${hs}")}
+ eval rtname=\${$(get_rtname "${rt}")}
+
+ ip netns exec "${hsname}" sysctl -wq net.ipv6.conf.all.accept_dad=0
+ ip netns exec "${hsname}" sysctl -wq net.ipv6.conf.default.accept_dad=0
+
+ ip -netns "${hsname}" link add "${HS_VETH_NAME}" type veth \
+ peer name "${RT2HS_DEVNAME}" netns "${rtname}"
+
+ ip -netns "${hsname}" addr add "${IPv6_HS_NETWORK}::${hs}/64" \
+ dev "${HS_VETH_NAME}" nodad
+ ip -netns "${hsname}" addr add "${IPv4_HS_NETWORK}.${hs}/24" \
+ dev "${HS_VETH_NAME}"
+
+ ip -netns "${hsname}" link set "${HS_VETH_NAME}" up
+ ip -netns "${hsname}" link set lo up
+
+ # veth-hs is a bridge port; IPs go on br0 (see setup_bridge)
+ ip -netns "${rtname}" link set "${RT2HS_DEVNAME}" up
+}
+
+# Create srl2 device, bridge, and wire them together.
+# The srl2 device encapsulates L2 frames in IPv6+SRH towards the
+# remote router's End.DT2U SID. The bridge connects the host (via
+# veth-hs) with the SRv6 tunnel (via srl2-0).
+# args:
+# $1 - router id
+# $2 - remote router id (for SID list)
+setup_bridge()
+{
+ local rt="$1"
+ local remote_rt="$2"
+ local nsname
+
+ eval nsname=\${$(get_rtname "${rt}")}
+
+ # create the srl2 device pointing to the remote End.DT2U SID
+ ip -netns "${nsname}" link add "${SRL2_DEVNAME}" type srl2 \
+ segs "${VPN_LOCATOR_SERVICE}:${remote_rt}::${DT2U_FUNC}"
+ ip -netns "${nsname}" link set "${SRL2_DEVNAME}" up
+
+ # create bridge and add ports
+ ip -netns "${nsname}" link add "${BRIDGE_DEVNAME}" type bridge
+ ip -netns "${nsname}" link set "${BRIDGE_DEVNAME}" up
+
+ ip -netns "${nsname}" link set "${RT2HS_DEVNAME}" master \
+ "${BRIDGE_DEVNAME}"
+ ip -netns "${nsname}" link set "${SRL2_DEVNAME}" master \
+ "${BRIDGE_DEVNAME}"
+
+ # IP addresses on br0 (gateway for the hosts)
+ ip -netns "${nsname}" addr add "${IPv6_HS_NETWORK}::254/64" \
+ dev "${BRIDGE_DEVNAME}" nodad
+ ip -netns "${nsname}" addr \
+ add "${IPv4_HS_NETWORK}.254/24" dev "${BRIDGE_DEVNAME}"
+}
+
+setup()
+{
+ local i
+
+ # create routers
+ ROUTERS="1 2"; readonly ROUTERS
+ for i in ${ROUTERS}; do
+ create_router "${i}"
+ done
+
+ # create hosts
+ HOSTS="1 2"; readonly HOSTS
+ for i in ${HOSTS}; do
+ create_host "${i}"
+ done
+
+ # set up the links for connecting routers
+ add_link_rt_pairs 1 "2"
+
+ # set up the basic connectivity of routers and routes required for
+ # reachability of SIDs.
+ setup_rt_networking 1 "2"
+ setup_rt_networking 2 "1"
+
+ # set up the hosts connected to routers
+ setup_hs 1 1
+ setup_hs 2 2
+
+ # set up srl2 devices and bridges on each router.
+ # rt-1's srl2-0 encapsulates towards rt-2's End.DT2U SID and
+ # vice versa.
+ setup_bridge 1 2
+ setup_bridge 2 1
+
+ # set up SRv6 Endpoints (i.e. SRv6 End.DT2U)
+ setup_rt_local_sids 1 "2"
+ setup_rt_local_sids 2 "1"
+
+ # testing environment was set up successfully
+ SETUP_ERR=0
+}
+
+check_rt_connectivity()
+{
+ local rtsrc="$1"
+ local rtdst="$2"
+ local prefix
+ local rtsrc_nsname
+
+ eval rtsrc_nsname=\${$(get_rtname "${rtsrc}")}
+
+ prefix="$(get_network_prefix "${rtsrc}" "${rtdst}")"
+
+ ip netns exec "${rtsrc_nsname}" ping -c 1 -W "${PING_TIMEOUT_SEC}" \
+ "${prefix}::${rtdst}" >/dev/null 2>&1
+}
+
+check_and_log_rt_connectivity()
+{
+ local rtsrc="$1"
+ local rtdst="$2"
+
+ check_rt_connectivity "${rtsrc}" "${rtdst}"
+ log_test $? 0 "Routers connectivity: rt-${rtsrc} -> rt-${rtdst}"
+}
+
+check_hs_ipv6_connectivity()
+{
+ local hssrc="$1"
+ local hsdst="$2"
+ local hssrc_nsname
+
+ eval hssrc_nsname=\${$(get_hsname "${hssrc}")}
+
+ ip netns exec "${hssrc_nsname}" ping -c 1 -W "${PING_TIMEOUT_SEC}" \
+ "${IPv6_HS_NETWORK}::${hsdst}" >/dev/null 2>&1
+}
+
+check_hs_ipv4_connectivity()
+{
+ local hssrc="$1"
+ local hsdst="$2"
+ local hssrc_nsname
+
+ eval hssrc_nsname=\${$(get_hsname "${hssrc}")}
+
+ ip netns exec "${hssrc_nsname}" ping -c 1 -W "${PING_TIMEOUT_SEC}" \
+ "${IPv4_HS_NETWORK}.${hsdst}" >/dev/null 2>&1
+}
+
+check_and_log_hs2gw_connectivity()
+{
+ local hssrc="$1"
+
+ check_hs_ipv6_connectivity "${hssrc}" 254
+ log_test $? 0 "IPv6 Hosts connectivity: hs-${hssrc} -> gw"
+
+ check_hs_ipv4_connectivity "${hssrc}" 254
+ log_test $? 0 "IPv4 Hosts connectivity: hs-${hssrc} -> gw"
+}
+
+check_and_log_hs_ipv6_connectivity()
+{
+ local hssrc="$1"
+ local hsdst="$2"
+
+ check_hs_ipv6_connectivity "${hssrc}" "${hsdst}"
+ log_test $? 0 "IPv6 Hosts connectivity: hs-${hssrc} -> hs-${hsdst}"
+}
+
+check_and_log_hs_ipv4_connectivity()
+{
+ local hssrc="$1"
+ local hsdst="$2"
+
+ check_hs_ipv4_connectivity "${hssrc}" "${hsdst}"
+ log_test $? 0 "IPv4 Hosts connectivity: hs-${hssrc} -> hs-${hsdst}"
+}
+
+check_and_log_hs_connectivity()
+{
+ local hssrc="$1"
+ local hsdst="$2"
+
+ check_and_log_hs_ipv4_connectivity "${hssrc}" "${hsdst}"
+ check_and_log_hs_ipv6_connectivity "${hssrc}" "${hsdst}"
+}
+
+router_tests()
+{
+ local i
+ local j
+
+ log_section "IPv6 routers connectivity test"
+
+ for i in ${ROUTERS}; do
+ for j in ${ROUTERS}; do
+ if [ "${i}" -eq "${j}" ]; then
+ continue
+ fi
+
+ check_and_log_rt_connectivity "${i}" "${j}"
+ done
+ done
+}
+
+host2gateway_tests()
+{
+ local hs
+
+ log_section "IPv4/IPv6 connectivity test among hosts and gateways"
+
+ for hs in ${HOSTS}; do
+ check_and_log_hs2gw_connectivity "${hs}"
+ done
+}
+
+host_vpn_tests()
+{
+ log_section "SRv6 srl2 + End.DT2U L2 VPN connectivity test hosts (h1 <-> h2)"
+
+ check_and_log_hs_connectivity 1 2
+ check_and_log_hs_connectivity 2 1
+}
+
+test_dummy_dev_or_ksft_skip()
+{
+ local test_netns
+
+ test_netns="dummy-$(mktemp -u XXXXXXXX)"
+
+ if ! ip netns add "${test_netns}"; then
+ echo "SKIP: Cannot set up netns for testing dummy dev support"
+ exit "${ksft_skip}"
+ fi
+
+ modprobe dummy &>/dev/null || true
+ if ! ip -netns "${test_netns}" link \
+ add "${DUMMY_DEVNAME}" type dummy; then
+ echo "SKIP: dummy dev not supported"
+
+ ip netns del "${test_netns}"
+ exit "${ksft_skip}"
+ fi
+
+ ip netns del "${test_netns}"
+}
+
+test_srl2_dev_or_ksft_skip()
+{
+ local test_netns
+
+ test_netns="srl2-$(mktemp -u XXXXXXXX)"
+
+ if ! ip netns add "${test_netns}"; then
+ echo "SKIP: Cannot set up netns for testing srl2 dev support"
+ exit "${ksft_skip}"
+ fi
+
+ modprobe srl2 &>/dev/null || true
+ if ! ip -netns "${test_netns}" link \
+ add srl2-test type srl2 \
+ segs fc00::1; then
+ echo "SKIP: srl2 dev not supported"
+
+ ip netns del "${test_netns}"
+ exit "${ksft_skip}"
+ fi
+
+ ip netns del "${test_netns}"
+}
+
+test_iproute2_supp_or_ksft_skip()
+{
+ if ! ip route help 2>&1 | grep -qo "End.DT2U"; then
+ echo "SKIP: Missing SRv6 End.DT2U support in iproute2"
+ exit "${ksft_skip}"
+ fi
+
+ if ! ip link help srl2 2>&1 | grep -qo "srl2"; then
+ echo "SKIP: Missing srl2 link type support in iproute2"
+ exit "${ksft_skip}"
+ fi
+}
+
+if [ "$(id -u)" -ne 0 ]; then
+ echo "SKIP: Need root privileges"
+ exit "${ksft_skip}"
+fi
+
+# required programs to carry out this selftest
+test_command_or_ksft_skip ip
+test_command_or_ksft_skip ping
+test_command_or_ksft_skip sysctl
+test_command_or_ksft_skip grep
+
+test_iproute2_supp_or_ksft_skip
+test_dummy_dev_or_ksft_skip
+test_srl2_dev_or_ksft_skip
+
+set -e
+trap cleanup EXIT
+
+setup
+set +e
+
+router_tests
+host2gateway_tests
+host_vpn_tests
+
+print_log_test_results
--
2.20.1