summaryrefslogtreecommitdiff
path: root/tools/testing/selftests/net/openvswitch/openvswitch.sh
diff options
context:
space:
mode:
Diffstat (limited to 'tools/testing/selftests/net/openvswitch/openvswitch.sh')
-rwxr-xr-xtools/testing/selftests/net/openvswitch/openvswitch.sh549
1 files changed, 547 insertions, 2 deletions
diff --git a/tools/testing/selftests/net/openvswitch/openvswitch.sh b/tools/testing/selftests/net/openvswitch/openvswitch.sh
index b327d3061ed5..a31f7fb6882d 100755
--- a/tools/testing/selftests/net/openvswitch/openvswitch.sh
+++ b/tools/testing/selftests/net/openvswitch/openvswitch.sh
@@ -27,6 +27,13 @@ tests="
upcall_interfaces ovs: test the upcall interfaces
tunnel_metadata ovs: test extraction of tunnel metadata
drop_reason drop: test drop reasons are emitted
+ pop_vlan vlan: POP_VLAN action strips tag
+ dec_ttl ttl: dec_ttl decrements IP TTL
+ flow_set flow-set: Flow modify
+ action_set set: SET action rewrites fields
+ trunc trunc: output truncation
+ icmpv6 icmpv6: ICMPv6 echo type match
+ sctp_connect_v4 sctp: SCTP flow key matching
psample psample: Sampling packets with psample"
info() {
@@ -190,6 +197,23 @@ ovs_add_flow () {
return 0
}
+ovs_mod_flow () {
+ if [ -n "$4" ]; then
+ info "Modifying flow: sbx:$1 br:$2 flow:$3 act:$4"
+ ovs_sbx "$1" python3 $ovs_base/ovs-dpctl.py \
+ mod-flow "$2" "$3" "$4"
+ else
+ info "Modifying flow (no actions): sbx:$1 br:$2 flow:$3"
+ ovs_sbx "$1" python3 $ovs_base/ovs-dpctl.py \
+ mod-flow "$2" "$3"
+ fi
+ if [ $? -ne 0 ]; then
+ info "Flow modify [ $3 ] failed"
+ return 1
+ fi
+ return 0
+}
+
ovs_del_flows () {
info "Deleting all flows from DP: sbx:$1 br:$2"
ovs_sbx "$1" python3 $ovs_base/ovs-dpctl.py del-flows "$2"
@@ -243,6 +267,439 @@ usage() {
}
+test_dec_ttl() {
+ sbx_add "test_dec_ttl" || return $?
+ ovs_add_dp "test_dec_ttl" decttl || return 1
+
+ info "create namespaces"
+ for ns in client server; do
+ ovs_add_netns_and_veths "test_dec_ttl" "decttl" "$ns" \
+ "${ns:0:1}0" "${ns:0:1}1" || return 1
+ done
+
+ ip netns exec client ip addr add 10.0.0.1/24 dev c1
+ ip netns exec client ip link set c1 up
+ ip netns exec server ip addr add 10.0.0.2/24 dev s1
+ ip netns exec server ip link set s1 up
+
+ # Probe: check if kernel supports dec_ttl action.
+ ovs_add_flow "test_dec_ttl" decttl \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' \
+ 'dec_ttl(le_1())' &>/dev/null
+ if [ $? -ne 0 ]; then
+ info "no support for dec_ttl - skipping"
+ ovs_exit_sig
+ return $ksft_skip
+ fi
+
+ ovs_del_flows "test_dec_ttl" decttl
+
+ # ARP flows (bidirectional)
+ ovs_add_flow "test_dec_ttl" decttl \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
+ ovs_add_flow "test_dec_ttl" decttl \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
+
+ # IP flows with dec_ttl action
+ ovs_add_flow "test_dec_ttl" decttl \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' \
+ 'dec_ttl(le_1()),2' || return 1
+ ovs_add_flow "test_dec_ttl" decttl \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' \
+ 'dec_ttl(le_1()),1' || return 1
+
+ info "verify connectivity with dec_ttl"
+ ovs_sbx "test_dec_ttl" ip netns exec client ping -c 1 -W 2 \
+ 10.0.0.2 || return 1
+
+ info "verify TTL=1 is dropped by dec_ttl"
+ ovs_sbx "test_dec_ttl" ip netns exec client ping -c 1 -W 2 \
+ -t 1 10.0.0.2 >/dev/null 2>&1 \
+ && { info "FAIL: ping should fail with TTL=1 and dec_ttl"
+ return 1; }
+
+ return 0
+}
+
+test_flow_set() {
+ sbx_add "test_flow_set" || return $?
+ ovs_add_dp "test_flow_set" flowset || return 1
+
+ info "create namespaces"
+ for ns in client server; do
+ ovs_add_netns_and_veths "test_flow_set" "flowset" "$ns" \
+ "${ns:0:1}0" "${ns:0:1}1" || return 1
+ done
+
+ ip netns exec client ip addr add 10.0.0.1/24 dev c1
+ ip netns exec client ip link set c1 up
+ ip netns exec server ip addr add 10.0.0.2/24 dev s1
+ ip netns exec server ip link set s1 up
+
+ ovs_add_flow "test_flow_set" flowset \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
+ ovs_add_flow "test_flow_set" flowset \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
+
+ local fwd_flow="ufid:00000001-0002-0003-0004-000500060007"
+ fwd_flow="$fwd_flow,in_port(1),eth(),eth_type(0x0800),ipv4()"
+
+ ovs_add_flow "test_flow_set" flowset "$fwd_flow" '2' \
+ || return 1
+ ovs_add_flow "test_flow_set" flowset \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1
+
+ info "verify initial forwarding"
+ ovs_sbx "test_flow_set" ip netns exec client ping -c 1 -W 2 \
+ 10.0.0.2 || return 1
+
+ info "mod-flow with new actions (change to drop)"
+ ovs_mod_flow "test_flow_set" flowset "$fwd_flow" 'drop' \
+ || return 1
+
+ info "verify traffic is now dropped"
+ ovs_sbx "test_flow_set" ip netns exec client ping -c 1 -W 2 \
+ 10.0.0.2 >/dev/null 2>&1 \
+ && { info "FAIL: ping should fail after mod-flow to drop"
+ return 1; }
+
+ info "mod-flow without actions"
+ ovs_mod_flow "test_flow_set" flowset "$fwd_flow" || return 1
+
+ info "verify flow retained drop action via dump"
+ python3 "$ovs_base/ovs-dpctl.py" dump-flows flowset \
+ | grep -q "actions:drop" || \
+ { info "FAIL: flow not showing drop action"; return 1; }
+
+ info "verify drop actions unchanged"
+ ovs_sbx "test_flow_set" ip netns exec client ping -c 1 -W 2 \
+ 10.0.0.2 >/dev/null 2>&1 \
+ && { info "FAIL: ping should still fail after no-actions set"
+ return 1; }
+
+ return 0
+}
+
+test_action_set() {
+ sbx_add "test_action_set" || return $?
+ ovs_add_dp "test_action_set" settest || return 1
+
+ info "create namespaces"
+ for ns in client server; do
+ ovs_add_netns_and_veths "test_action_set" "settest" "$ns" \
+ "${ns:0:1}0" "${ns:0:1}1" || return 1
+ done
+
+ ip netns exec client ip addr add 10.0.0.1/24 dev c1
+ ip netns exec client ip link set c1 up
+ ip netns exec server ip addr add 10.0.0.2/24 dev s1
+ ip netns exec server ip link set s1 up
+
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
+
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' '2' || return 1
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1
+
+ info "verify connectivity without SET"
+ ovs_sbx "test_action_set" ip netns exec client ping -c 1 -W 2 \
+ 10.0.0.2 || return 1
+
+ ovs_del_flows "test_action_set" settest
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
+
+ info "set ipv4 dst to unreachable address"
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' \
+ 'set(ipv4(dst=10.0.0.99)),2' || return 1
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1
+
+ info "verify ping fails with rewritten dst"
+ ovs_sbx "test_action_set" ip netns exec client ping -c 1 -W 2 \
+ 10.0.0.2 >/dev/null 2>&1 \
+ && { info "FAIL: ping should fail with dst rewritten"
+ return 1; }
+
+ ovs_del_flows "test_action_set" settest
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' '2' || return 1
+ ovs_add_flow "test_action_set" settest \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1
+
+ info "verify connectivity restored without SET"
+ ovs_sbx "test_action_set" ip netns exec client ping -c 1 -W 2 \
+ 10.0.0.2 || return 1
+
+ return 0
+}
+
+# trunc test
+# - trunc(14): truncate to ETH_HLEN, strips IP payload, ping fails
+# - trunc(1) and trunc(13): kernel rejects below ETH_HLEN (EINVAL)
+# - restore normal forwarding and verify recovery
+test_trunc() {
+ sbx_add "test_trunc" || return $?
+ ovs_add_dp "test_trunc" trunctest || return 1
+
+ info "create namespaces"
+ for ns in client server; do
+ ovs_add_netns_and_veths "test_trunc" "trunctest" \
+ "$ns" "${ns:0:1}0" "${ns:0:1}1" || return 1
+ done
+
+ ip netns exec client ip addr add 10.0.0.1/24 dev c1
+ ip netns exec client ip link set c1 up
+ ip netns exec server ip addr add 10.0.0.2/24 dev s1
+ ip netns exec server ip link set s1 up
+
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' \
+ '2' || return 1
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' \
+ '1' || return 1
+
+ info "verify connectivity without truncation"
+ ovs_sbx "test_trunc" ip netns exec client \
+ ping -c 1 -W 2 10.0.0.2 || return 1
+
+ # trunc below ETH_HLEN must be rejected by the kernel
+ info "verify trunc(1) is rejected"
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' \
+ 'trunc(1),2' &> /dev/null \
+ && { info "trunc(1) should be rejected"; return 1; }
+
+ info "verify trunc(13) is rejected"
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' \
+ 'trunc(13),2' &> /dev/null \
+ && { info "trunc(13) should be rejected"; return 1; }
+
+ ovs_del_flows "test_trunc" trunctest
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
+
+ info "add trunc(14) forwarding flow"
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' \
+ 'trunc(14),2' || return 1
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' \
+ '1' || return 1
+
+ info "verify ping fails with trunc(14)"
+ ovs_sbx "test_trunc" ip netns exec client \
+ ping -c 1 -W 2 10.0.0.2 >/dev/null 2>&1 \
+ && { info "ping should fail with trunc(14)"
+ return 1; }
+
+ ovs_del_flows "test_trunc" trunctest
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' \
+ '2' || return 1
+ ovs_add_flow "test_trunc" trunctest \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' \
+ '1' || return 1
+
+ info "verify connectivity restored"
+ ovs_sbx "test_trunc" ip netns exec client \
+ ping -c 1 -W 2 10.0.0.2 || return 1
+
+ return 0
+}
+
+# icmpv6 test
+# - static neighbours to bypass NDP (nud permanent)
+# - icmpv6(type=128) echo request, icmpv6(type=129) echo reply
+# - remove flows and verify ping fails, reinstall and recover
+test_icmpv6() {
+ local t="test_icmpv6"
+ local v6="eth_type(0x86dd),ipv6(proto=58)"
+
+ sbx_add "$t" || return $?
+ ovs_add_dp "$t" icmpv6 || return 1
+
+ info "create namespaces"
+ for ns in client server; do
+ ovs_add_netns_and_veths "$t" "icmpv6" \
+ "$ns" "${ns:0:1}0" "${ns:0:1}1" || return 1
+ done
+
+ ip netns exec client ip addr add fd00::1/64 dev c1 nodad
+ ip netns exec client ip link set c1 up
+ ip netns exec server ip addr add fd00::2/64 dev s1 nodad
+ ip netns exec server ip link set s1 up
+
+ local cl_mac sl_mac
+ cl_mac=$(ip netns exec client ip link show c1 \
+ | awk '/link\/ether/ {print $2}')
+ [ -z "$cl_mac" ] && \
+ { info "failed to get c1 hwaddr"; return 1; }
+ sl_mac=$(ip netns exec server ip link show s1 \
+ | awk '/link\/ether/ {print $2}')
+ [ -z "$sl_mac" ] && \
+ { info "failed to get s1 hwaddr"; return 1; }
+ ip netns exec client ip -6 neigh add fd00::2 \
+ lladdr "$sl_mac" nud permanent dev c1 || return 1
+ ip netns exec server ip -6 neigh add fd00::1 \
+ lladdr "$cl_mac" nud permanent dev s1 || return 1
+
+ # Probe: check if kernel supports icmpv6 flow key.
+ ovs_add_flow "$t" icmpv6 \
+ "in_port(1),eth(),$v6,icmpv6(type=128)" \
+ '2' &>/dev/null
+ if [ $? -ne 0 ]; then
+ info "no support for icmpv6 key - skipping"
+ ovs_exit_sig
+ return $ksft_skip
+ fi
+ ovs_del_flows "$t" icmpv6
+
+ ovs_add_flow "$t" icmpv6 \
+ "in_port(1),eth(),$v6,icmpv6(type=128)" \
+ '2' || return 1
+ ovs_add_flow "$t" icmpv6 \
+ "in_port(2),eth(),$v6,icmpv6(type=129)" \
+ '1' || return 1
+
+ info "verify ICMPv6 echo with type-specific flows"
+ ovs_sbx "$t" ip netns exec client \
+ ping -6 -c 1 -W 2 fd00::2 || return 1
+
+ ovs_del_flows "$t" icmpv6
+
+ info "verify ping fails without echo flows"
+ ovs_sbx "$t" ip netns exec client \
+ ping -6 -c 1 -W 2 fd00::2 >/dev/null 2>&1 \
+ && { info "ping should fail without flows"
+ return 1; }
+
+ ovs_add_flow "$t" icmpv6 \
+ "in_port(1),eth(),$v6,icmpv6(type=128)" \
+ '2' || return 1
+ ovs_add_flow "$t" icmpv6 \
+ "in_port(2),eth(),$v6,icmpv6(type=129)" \
+ '1' || return 1
+
+ info "verify connectivity restored"
+ ovs_sbx "$t" ip netns exec client \
+ ping -6 -c 1 -W 2 fd00::2 || return 1
+
+ return 0
+}
+
+# Check for an SCTP endpoint via /proc, which works without sctp_diag.
+sctp_eps_has() {
+ ip netns exec "$1" awk -v p="$2" '$6==p' /proc/net/sctp/eps | grep -q .
+}
+
+# sctp_connect_v4 test
+# - sctp(dst=4443) matches client-to-server INIT
+# - sctp(src=4443) matches server-to-client INIT-ACK
+# - remove flows and verify connection fails, reinstall and recover
+test_sctp_connect_v4() {
+ local t="test_sctp_connect_v4"
+ local srv_ip=172.31.110.20
+
+ modprobe -q sctp 2>/dev/null || return "$ksft_skip"
+ socat -V 2>&1 | grep -q "define WITH_SCTP" || return "$ksft_skip"
+
+ sbx_add "$t" || return $?
+ ovs_add_dp "$t" sctp4 || return 1
+
+ info "create namespaces"
+ for ns in client server; do
+ ovs_add_netns_and_veths "$t" "sctp4" "$ns" \
+ "${ns:0:1}0" "${ns:0:1}1" || return 1
+ done
+
+ ip netns exec client ip addr add 172.31.110.10/24 dev c1
+ ip netns exec client ip link set c1 up
+ ip netns exec server ip addr add "${srv_ip}/24" dev s1
+ ip netns exec server ip link set s1 up
+
+ # ARP forwarding
+ ovs_add_flow "$t" sctp4 \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' \
+ '2' || return 1
+ ovs_add_flow "$t" sctp4 \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' \
+ '1' || return 1
+
+ # SCTP port matching: dst for request, src for reply
+ ovs_add_flow "$t" sctp4 \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4(proto=132),sctp(dst=4443)' \
+ '2' || return 1
+ ovs_add_flow "$t" sctp4 \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4(proto=132),sctp(src=4443)' \
+ '1' || return 1
+
+ # The listener forks a child per association, so one instance serves
+ # the whole test and the flows stay the only variable. -t 1 bounds
+ # how long a child lingers after its association closes.
+ ovs_netns_spawn_daemon "$t" "server" \
+ socat -u -t 1 SCTP4-LISTEN:4443,fork STDOUT
+ ovs_wait sctp_eps_has server 4443 || return 1
+
+ info "verify SCTP association with port-keyed flows"
+ ovs_sbx "$t" ip netns exec client \
+ timeout 3 socat -u STDIN "SCTP4-CONNECT:${srv_ip}:4443" </dev/null \
+ || return 1
+
+ ovs_del_flows "$t" sctp4
+
+ info "verify connection fails without flows"
+ ovs_add_flow "$t" sctp4 \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' \
+ '2' || return 1
+ ovs_add_flow "$t" sctp4 \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' \
+ '1' || return 1
+
+ ovs_sbx "$t" ip netns exec client \
+ timeout 3 socat -u STDIN "SCTP4-CONNECT:${srv_ip}:4443" </dev/null \
+ >/dev/null 2>&1 \
+ && { info "connection should fail without flows"
+ return 1; }
+
+ info "reinstall flows and verify recovery"
+ ovs_add_flow "$t" sctp4 \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4(proto=132),sctp(dst=4443)' \
+ '2' || return 1
+ ovs_add_flow "$t" sctp4 \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4(proto=132),sctp(src=4443)' \
+ '1' || return 1
+
+ ovs_sbx "$t" ip netns exec client \
+ timeout 3 socat -u STDIN "SCTP4-CONNECT:${srv_ip}:4443" </dev/null \
+ || return 1
+
+ return 0
+}
+
# psample test
# - use psample to observe packets
test_psample() {
@@ -303,6 +760,8 @@ test_psample() {
# sFlow / IPFIX.
nlpid=$(grep -E "listening on upcall packet handler" \
$ovs_dir/s0.out | cut -d ":" -f 2 | tr -d ' ')
+ [ -z "$nlpid" ] && \
+ { info "failed to get upcall PID"; return 1; }
ovs_add_flow "test_psample" psample \
"in_port(2),eth(),eth_type(0x0800),ipv4()" \
@@ -336,6 +795,10 @@ test_drop_reason() {
ovs_drop_subsys=$(pahole -C skb_drop_reason_subsys |
awk '/OPENVSWITCH/ { print $3; }' |
tr -d ,)
+ if [ -z "$ovs_drop_subsys" ]; then
+ info "failed to get OVS drop subsys ID"
+ return $ksft_skip
+ fi
sbx_add "test_drop_reason" || return $?
@@ -434,13 +897,19 @@ test_arp_ping () {
# Setup client namespace
ip netns exec client ip addr add 172.31.110.10/24 dev c1
ip netns exec client ip link set c1 up
- HW_CLIENT=`ip netns exec client ip link show dev c1 | grep -E 'link/ether [0-9a-f:]+' | awk '{print $2;}'`
+ HW_CLIENT=$(ip netns exec client ip link show dev c1 \
+ | awk '/link\/ether/ {print $2}')
+ [ -z "$HW_CLIENT" ] && \
+ { info "failed to get client hwaddr"; return 1; }
info "Client hwaddr: $HW_CLIENT"
# Setup server namespace
ip netns exec server ip addr add 172.31.110.20/24 dev s1
ip netns exec server ip link set s1 up
- HW_SERVER=`ip netns exec server ip link show dev s1 | grep -E 'link/ether [0-9a-f:]+' | awk '{print $2;}'`
+ HW_SERVER=$(ip netns exec server ip link show dev s1 \
+ | awk '/link\/ether/ {print $2}')
+ [ -z "$HW_SERVER" ] && \
+ { info "failed to get server hwaddr"; return 1; }
info "Server hwaddr: $HW_SERVER"
ovs_add_flow "test_arp_ping" arpping \
@@ -830,6 +1299,82 @@ test_tunnel_metadata() {
return 0
}
+test_pop_vlan() {
+ local sbx="test_pop_vlan"
+ sbx_add "$sbx" || return $?
+ ovs_add_dp "$sbx" vlandp || return 1
+
+ ovs_add_netns_and_veths "$sbx" vlandp \
+ ns1 veth1 ns1veth 192.0.2.1/24 || return 1
+ ovs_add_netns_and_veths "$sbx" vlandp \
+ ns2 veth2 ns2veth 192.0.2.2/24 || return 1
+
+ # Baseline: untagged bidirectional forwarding
+ ovs_add_flow "$sbx" vlandp \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
+ ovs_add_flow "$sbx" vlandp \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
+ ovs_add_flow "$sbx" vlandp \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4()' '2' || return 1
+ ovs_add_flow "$sbx" vlandp \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1
+ ovs_sbx "$sbx" ip netns exec ns1 ping -c 3 -W 2 \
+ 192.0.2.2 || return 1
+
+ # VLAN topology: ns1 uses VLAN sub-interface, ns2 is plain
+ ip -n ns1 link add link ns1veth name ns1veth.10 \
+ type vlan id 10 || return 1
+ on_exit "ip -n ns1 link del ns1veth.10 2>/dev/null"
+ ip -n ns1 addr add 198.51.100.1/24 dev ns1veth.10 || return 1
+ ip -n ns1 link set ns1veth.10 up || return 1
+ ip -n ns2 addr add 198.51.100.2/24 dev ns2veth || return 1
+
+ ovs_del_flows "$sbx" vlandp
+
+ # Static ARP: avoids VLAN-tagged ARP complexity
+ local ns1veth10mac ns2mac
+ ns1veth10mac=$(ip -n ns1 link show ns1veth.10 \
+ | awk '/link\/ether/ {print $2}')
+ [ -z "$ns1veth10mac" ] && \
+ { info "failed to get ns1veth10mac"; return 1; }
+ ns2mac=$(ip -n ns2 link show ns2veth \
+ | awk '/link\/ether/ {print $2}')
+ [ -z "$ns2mac" ] && \
+ { info "failed to get ns2mac"; return 1; }
+ ip -n ns1 neigh replace 198.51.100.2 lladdr "$ns2mac" \
+ dev ns1veth.10 nud permanent || return 1
+ ip -n ns2 neigh replace 198.51.100.1 \
+ lladdr "$ns1veth10mac" \
+ dev ns2veth nud permanent || return 1
+
+ local vlan_match='in_port(1),eth(),eth_type(0x8100),'
+ vlan_match+='vlan(vid=10),'
+ vlan_match+='encap(eth_type(0x0800),'
+ vlan_match+='ipv4(src=198.51.100.1,proto=1),icmp())'
+
+ # Negative: forward without pop_vlan -- tagged frame
+ # is invisible to ns2 (no VLAN sub-interface), ping fails
+ ovs_add_flow "$sbx" vlandp "$vlan_match" '2' || return 1
+ ovs_sbx "$sbx" ip netns exec ns1 ping -I ns1veth.10 \
+ -c 3 -W 1 198.51.100.2 >/dev/null 2>&1 \
+ && { info "FAIL: ping should fail without pop_vlan"
+ return 1; }
+
+ ovs_del_flows "$sbx" vlandp
+
+ # Positive: pop_vlan strips tag on forward path,
+ # push_vlan restores tag on return path -- ping succeeds
+ ovs_add_flow "$sbx" vlandp \
+ "$vlan_match" 'pop_vlan,2' || return 1
+ ovs_add_flow "$sbx" vlandp \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' \
+ 'push_vlan(vid=10,pcp=0,tpid=0x8100),1' || return 1
+ ovs_sbx "$sbx" ip netns exec ns1 ping -I ns1veth.10 \
+ -c 3 -W 2 198.51.100.2 || return 1
+
+ return 0
+}
+
run_test() {
(
tname="$1"