Implemented a self-test for ipvlan in l2macnat mode.
The test verifies: 1) It's not possible to configure an ip in l2macnat mode on ipvtap
2) It creates several net namespaces - Default namespace emulates host, - ipvlan-tst-phy emulates some host in remote network - ipvlan-tst-0/1 emulate VMs on host.
Test verifies, that MAC addresses are as expected in ARP/NEIGH tables: all MACs in 'tst-phy' points to "host" mac-address all MACs in Default and tst are real ones
3) The l2macnat mode has limited number of addresses remembered on port. Test verifies, that this limit really works.
Signed-off-by: Dmitry Skorodumov skorodumov.dmitry@huawei.com --- tools/testing/selftests/net/Makefile | 3 + .../selftests/net/ipvtap_macnat_bridge.py | 174 +++++++++ .../selftests/net/ipvtap_macnat_test.sh | 332 ++++++++++++++++++ 3 files changed, 509 insertions(+) create mode 100755 tools/testing/selftests/net/ipvtap_macnat_bridge.py create mode 100755 tools/testing/selftests/net/ipvtap_macnat_test.sh
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile index b5127e968108..ff28012d34db 100644 --- a/tools/testing/selftests/net/Makefile +++ b/tools/testing/selftests/net/Makefile @@ -203,6 +203,9 @@ YNL_GEN_PROGS := netlink-dumps TEST_GEN_FILES += $(YNL_GEN_FILES) TEST_GEN_PROGS += $(YNL_GEN_PROGS)
+TEST_PROGS += ipvtap_macnat_test.sh +TEST_FILES += ipvtap_macnat_bridge.py + TEST_GEN_FILES += $(patsubst %.c,%.o,$(wildcard *.bpf.c))
TEST_INCLUDES := forwarding/lib.sh diff --git a/tools/testing/selftests/net/ipvtap_macnat_bridge.py b/tools/testing/selftests/net/ipvtap_macnat_bridge.py new file mode 100755 index 000000000000..6fc4762b03cd --- /dev/null +++ b/tools/testing/selftests/net/ipvtap_macnat_bridge.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +""" +Script to bridge ipvtap and tap, +needed to simulate behaviour of virtual machine using ipvtap. + +ipvtap in macnat mode cannot have IP address. +Due to limitations of ipvtap, it also cannot be plugged +into bridge. +Use this script to connect ipvtap and tap and assing IP to tap. +""" + +import socket +import os +import select +import sys +import signal +import fcntl +import struct +import subprocess + +# Linux TUN/TAP constants +TUNSETIFF = 0x400454ca +IFF_TUN = 0x0001 +IFF_TAP = 0x0002 +IFF_NO_PI = 0x1000 + +ns_name = "non-initialized" + +class TapBridge: + def __init__(self, tap, ipvtap, buffer_size=65536): + self.tap_name = tap + self.ipvtap_name = ipvtap + self.buffer_size = buffer_size + self.running = False + + def open_tap_file(self, path): + """Open TAP interface as a file""" + try: + return os.open(path, os.O_RDWR) + except Exception as e: + print(f"Error opening {path}: {e}") + return None + + def open_ipvtap_sock(self, tap_name): + """Open a TAP interface using raw socket""" + try: + sock = socket.socket(socket.AF_PACKET, + socket.SOCK_RAW, + socket.ntohs(0x0003)) + sock.bind((tap_name, 0)) + sock.setblocking(False) + print(f"Connected to IPVTAP interface: {tap_name}") + return sock + + except Exception as e: + print(f"Error opening IPVTAP interface {tap_name}: {e}") + return None + + def create_tap_interface(self, tap_name): + """Create and configure a TAP interface using /dev/net/tun""" + try: + # Open the tun device + tun_fd = os.open('/dev/net/tun', os.O_RDWR) + if tun_fd < 0: + raise Exception("Failed to open /dev/net/tun") + + # Prepare the ifr structure + tap_name_bytes = tap_name.encode('utf-8') + ifr = struct.pack('16sH', tap_name_bytes, IFF_TAP | IFF_NO_PI) + + # Set the interface name and flags + result = fcntl.ioctl(tun_fd, TUNSETIFF, ifr) + + # Get the actual interface name that was set + unpacked = struct.unpack('16sH', result) + actual_name = unpacked[0].split(b'\x00')[0].decode() + print(f"Created TAP interface: {actual_name}") + + return tun_fd + + except Exception as e: + print(f"Error creating TAP interface {tap_name}: {e}") + return None + + def forward_data(self, from_fd, to_fd, description): + """Forward data from one file descriptor to another""" + try: + data = os.read(from_fd, self.buffer_size) + if data: + os.write(to_fd, data) + return True + return False + + except BlockingIOError: + return True + except Exception as e: + print(f"Error forwarding data {description}: {e}") + return False + + def run(self): + """Main bridge loop""" + # Create TAP interfaces + tap1_fd = self.create_tap_interface(self.tap_name) + + sock = self.open_ipvtap_sock(self.ipvtap_name) + tap2_fd = sock.fileno() + + if tap1_fd is None or tap2_fd is None: + print("Failed to create TAP interfaces") + return + + print("Press Ctrl+C to stop\n") + + self.running = True + stats = {'tap1_to_tap2': 0, 'tap2_to_tap1': 0} + while self.running: + try: + # Use select to monitor both file descriptors + readable, _, _ = select.select([tap1_fd, tap2_fd], [], [], 1.0) + + for fd in readable: + if fd == tap1_fd: + descr = f"from {self.tap_name} to {self.ipvtap_name}" + if self.forward_data(tap1_fd, tap2_fd, descr): + stats['tap1_to_tap2'] += 1 + else: + self.running = False + elif fd == tap2_fd: + descr = f"from {self.ipvtap_name} to {self.tap_name}" + if self.forward_data(tap2_fd, tap1_fd, descr): + stats['tap2_to_tap1'] += 1 + else: + self.running = False + + except KeyboardInterrupt: + print("\nShutting down...") + self.running = False + except Exception as e: + print(f"Error in main loop: {e}") + self.running = False + + # Cleanup + os.close(tap1_fd) + os.close(tap2_fd) + print(f"Bridge stopped in {ns_name}. Stats: {stats}") + + +def signal_handler(_sig, _frame): + print(f'\nReceived interrupt signal, shutting down bridge in {ns_name}') + sys.exit(0) + + +if __name__ == "__main__": + ns_name = subprocess.getoutput("ip netns identify") or "default" + + signal.signal(signal.SIGINT, signal_handler) + + # Check if running as root + if os.geteuid() != 0: + print("ERROR: This script must be run as root!") + sys.exit(1) + + if len(sys.argv) != 3: + print("Usage: tap_bridge.py tap_name ipvtap_name") + sys.exit(1) + + TAP = sys.argv[1] + IPVTAP = sys.argv[2] + + print(f"Starting TAP bridge between {TAP} and {IPVTAP} in {ns_name}") + bridge = TapBridge(TAP, IPVTAP) + bridge.run() diff --git a/tools/testing/selftests/net/ipvtap_macnat_test.sh b/tools/testing/selftests/net/ipvtap_macnat_test.sh new file mode 100755 index 000000000000..5f684a6d7603 --- /dev/null +++ b/tools/testing/selftests/net/ipvtap_macnat_test.sh @@ -0,0 +1,332 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Tests for ipvtap in macnat mode + +NS_TST0=ipvlan-tst-0 +NS_TST1=ipvlan-tst-1 +NS_PHY=ipvlan-tst-phy + +IP_HOST=172.25.0.1 +IP_PHY=172.25.0.2 +IP_TST0=172.25.0.10 +IP_TST1=172.25.0.30 + +IP_OK0=("172.25.0.10" "172.25.0.11" "172.25.0.12" "172.25.0.13") +IP6_OK0=("fc00::10" "fc00::11" "fc00::12" "fc00::13" ) + +IP_OVFL0="172.25.0.14" +IP6_OVFL0="fc00::14" + +IP6_HOST=fc00::1 +IP6_PHY=fc00::2 +IP6_TST0=fc00::10 +IP6_TST1=fc00::30 + +MAC_HOST="92:3a:00:00:00:01" +MAC_PHY="92:3a:00:00:00:02" +MAC_TST0="92:3a:00:00:00:10" +MAC_TST1="92:3a:00:00:00:30" + +VETH_HOST=vethtst +VETH_PHY=vethtst.p + +# +# The testing environment looks this way: +# +# |------HOST------| |------PHY-------| +# | veth<----------------->veth | +# |------|--|------| |----------------| +# | | +# | | |-----TST0-------| +# | |------------|----ipvtap | +# | |----------------| +# | +# | |-----TST1-------| +# |---------------|----ivtap | +# |----------------| +# +# The macnat mode is for virtual machines, so ipvtap-interface is supposed +# to be used only for traffic monitoring and doesn't have ip-address. +# +# To simulate a virtual machine on ipvtap, we create TAP-interfaces +# in TST environments and assing IP-addresses to them. +# TAP and IPVTAP are connected with simple python script. +# + +ns_run() { + ns=$1 + shift + if [[ "$ns" == "default" ]]; then + "$@" >/dev/null + else + ip netns exec "$ns" "$@" >/dev/null + fi +} + +configure_ns() { + local ns=$1 + local n=$2 + local ip=$3 + local ip6=$4 + local mac=$5 + + ns_run $ns ip link set lo up + + if ! ip link add netns $ns name ipvtap0.$n link $VETH_HOST \ + type ipvtap mode l2macnat bridge; then + exit_error "FAIL: Failed to configure ipvtap link." + fi + ns_run $ns ip link set ipvtap0.$n up + + ns_run $ns ip tuntap add mode tap tap0.$n + ns_run $ns ip link set dev tap0.$n address $mac + # disable dad + ns_run $ns sysctl -w net/ipv6/conf/tap0.$n/accept_dad=0 + ns_run $ns ip link set tap0.$n up + ns_run $ns ip a a $ip/24 dev tap0.$n + ns_run $ns ip a a $ip6/64 dev tap0.$n +} + +start_macnat_bridge() { + local ns=$1 + local n=$2 + ip netns exec $ns python3 ipvtap_macnat_bridge.py tap0.$n ipvtap0.$n & +} + +configure_veth() { + local ns=$1 + local veth=$2 + local ip=$3 + local ip6=$4 + local mac=$5 + + ns_run $ns ip link set lo up + ns_run $ns ethtool -K $veth tx off rx off + ns_run $ns ip link set dev $veth address $mac + ns_run $ns ip link set $veth up + ns_run $ns ip a a $ip/24 dev $veth + ns_run $ns ip a a $ip6/64 dev $veth +} + +setup_env() { + ip netns add $NS_TST0 + ip netns add $NS_TST1 + ip netns add $NS_PHY + + # setup simulated other-host (phy) and host itself + ip link add $VETH_HOST type veth peer name $VETH_PHY \ + netns $NS_PHY >/dev/null + + # host config + configure_veth default $VETH_HOST $IP_HOST $IP6_HOST $MAC_HOST + configure_veth $NS_PHY $VETH_PHY $IP_PHY $IP6_PHY $MAC_PHY + + # TST namespaces config + configure_ns $NS_TST0 0 $IP_TST0 $IP6_TST0 $MAC_TST0 + configure_ns $NS_TST1 1 $IP_TST1 $IP6_TST1 $MAC_TST1 +} + +ping_all() { + # This will learn MAC/IP addresses on ipvtap + local ns=$1 + + ns_run $ns ping -c 1 $IP_TST0 + ns_run $ns ping -c 1 $IP6_TST0 + + ns_run $ns ping -c 1 $IP_TST1 + ns_run $ns ping -c 1 $IP6_TST1 + + ns_run $ns ping -c 1 $IP_HOST + ns_run $ns ping -c 1 $IP6_HOST + + ns_run $ns ping -c 1 $IP_PHY + ns_run $ns ping -c 1 $IP6_PHY +} + +check_mac_eq() { + # Ensure IP corresponds to MAC. + local ns=$1 + local ip=$2 + local mac=$3 + local dev=$4 + + if [[ "$ns" == "default" ]]; then + out=$( + ip neigh show $ip dev $dev \ + | grep "$ip" \ + | grep "$mac" + ) + else + out=$( + ip netns exec $ns \ + ip neigh show $ip dev $dev \ + | grep "$ip" \ + | grep "$mac" + ) + fi + + if [[ 'X'$out'X' == "XX" ]]; then + exit_error "FAIL: '$ip' is not '$mac'" + fi +} + +cleanup_env() { + ip link del $VETH_HOST + ip netns del $NS_TST0 + ip netns del $NS_TST1 + ip netns del $NS_PHY +} + +exit_error() { + echo $1 + exit 1 +} + +test_check_mac() { + # All IPs in NS_PHY should have MAC of the host + check_mac_eq $NS_PHY $IP_TST0 $MAC_HOST $VETH_PHY + check_mac_eq $NS_PHY $IP6_TST0 $MAC_HOST $VETH_PHY + check_mac_eq $NS_PHY $IP_TST1 $MAC_HOST $VETH_PHY + check_mac_eq $NS_PHY $IP6_TST1 $MAC_HOST $VETH_PHY + check_mac_eq $NS_PHY $IP_HOST $MAC_HOST $VETH_PHY + check_mac_eq $NS_PHY $IP6_HOST $MAC_HOST $VETH_PHY + + # All IPs in TST0 should have corresponding MAC + check_mac_eq $NS_TST0 $IP_HOST $MAC_HOST tap0.0 + check_mac_eq $NS_TST0 $IP6_HOST $MAC_HOST tap0.0 + check_mac_eq $NS_TST0 $IP_TST1 $MAC_TST1 tap0.0 + check_mac_eq $NS_TST0 $IP6_TST1 $MAC_TST1 tap0.0 + check_mac_eq $NS_TST0 $IP_PHY $MAC_PHY tap0.0 + check_mac_eq $NS_TST0 $IP6_PHY $MAC_PHY tap0.0 + + # All IPs in host should have corresponding MAC + check_mac_eq default $IP_TST0 $MAC_TST0 $VETH_HOST + check_mac_eq default $IP6_TST0 $MAC_TST0 $VETH_HOST + check_mac_eq default $IP_TST1 $MAC_TST1 $VETH_HOST + check_mac_eq default $IP6_TST1 $MAC_TST1 $VETH_HOST + check_mac_eq default $IP_PHY $MAC_PHY $VETH_HOST + check_mac_eq default $IP6_PHY $MAC_PHY $VETH_HOST +} + +test_ip_add() { + # adding IPs to ipvtap should be forbidden and should fail + if ns_run $NS_TST0 ip a a 172.26.0.1/24 dev ipvtap0.0; then + exit_error "FAIL: Module allowed to add ip to ipvtap." + fi + + if ns_run $NS_TST0 ip a a fc01::1/64 dev ipvtap0.0; then + exit_error "FAIL: Module allowed to add ip6 to ipvtap." + fi +} + +test_ip_overflow() { + # The ipvtap remembers limited number of addresses on interface. + # Let's overflow it and check that oldest one doesn't work. + + ns_run $NS_TST0 ip addr flush dev tap0.0 + + # Add exactly 4 ip addresses + for ip in "${IP_OK0[@]}"; do + ns_run $NS_TST0 ip a a $ip/24 dev tap0.0 + ns_run $NS_TST0 ping -c 1 $IP_HOST -I $ip + done + + # Initial check that ping works + if ! ping -c 2 $IP_TST0; then + exit_error "FAIL: Failed to ping tst0" + fi + + # Add 1 more ip addresses + ns_run $NS_TST0 ip a a $IP_OVFL0/24 dev tap0.0 + ns_run $NS_TST0 ping -c 1 $IP_HOST -I $IP_OVFL0 + # check that ping to oldest one from host fails. + echo "the next ping should fail:" + if ping -c 2 $IP_TST0; then + exit_error "FAIL: IP-0 still exists on interface" + fi + + # ping host using address-0 and force relearn of IP0. + # Host should be able ping after that + ns_run $NS_TST0 ping -c 1 $IP_HOST -I $IP_TST0 + + if ! ping -c 2 $IP_TST0; then + exit_error "FAIL: Failed to ping tst0 at stage 3" + fi +} + +test_ip6_overflow() { + # The ipvtap stores limited number of addresses on interface. + # Let's overflow it and check that oldest one doesn't work. + + ns_run $NS_TST0 ip addr flush dev tap0.0 + + # Add exactly 4 ip addresses + for ip6 in "${IP6_OK0[@]}"; do + ns_run $NS_TST0 ip a a $ip6/64 dev tap0.0 + ns_run $NS_TST0 ping -c 1 $IP6_HOST -I $ip6 + done + + # Initial check that ping6 works + if ! ping -c 2 $IP6_TST0; then + exit_error "FAIL: Failed to ping6 tst0" + fi + + # Add 1 more ip6 addresses + ns_run $NS_TST0 ip a a $IP6_OVFL0/64 dev tap0.0 + ns_run $NS_TST0 ping -c 1 $IP6_HOST -I $IP6_OVFL0 + # check that ping to oldest one from host fails. + echo "the next ping should fail:" + if ping -c 2 $IP6_TST0; then + exit_error "FAIL: IP6-0 still exists on interface" + fi + + # ping host using address-0 and force relearn of IP0. + # Host should be able ping after that + ns_run $NS_TST0 ping -c 1 $IP6_HOST -I $IP6_TST0 + if ! ping -c 2 $IP6_TST0; then + exit_error "FAIL: Failed to ping6 tst0 at stage 3" + fi +} + +exec_test() { + echo "TEST: "$2 + $1 + echo "PASSED: "$2 +} + +trap cleanup_env EXIT + +echo "ipvlan macnat tests" +echo "===================" + +modprobe -q tap +modprobe -q ipvlan +modprobe -q ipvtap + +setup_env + +exec_test test_ip_add "ip add not allowed" + +start_macnat_bridge $NS_TST0 0 +mb_pid1=$! +start_macnat_bridge $NS_TST1 1 +mb_pid2=$! + +echo "<<< Preparation: pinging all...." +ping_all default +ping_all $NS_TST0 +ping_all $NS_TST1 +ping_all $NS_PHY +echo "Finished preparational pinging all. >>>" + +exec_test test_check_mac "mac correctness" +exec_test test_ip_overflow "ip learn capacity overflow" +exec_test test_ip6_overflow "ip6 learn capacity overflow" + +kill -INT $mb_pid1 +kill -INT $mb_pid2 +wait $mb_pid1 +wait $mb_pid2 + +echo "All tests passed"
linux-kselftest-mirror@lists.linaro.org