diff options
Diffstat (limited to 'tests/sys/common')
| -rw-r--r-- | tests/sys/common/Makefile | 12 | ||||
| -rwxr-xr-x | tests/sys/common/divert.py | 82 | ||||
| -rwxr-xr-x | tests/sys/common/net_receiver.py | 115 | ||||
| -rwxr-xr-x | tests/sys/common/sender.py | 202 | ||||
| -rw-r--r-- | tests/sys/common/vnet.subr | 137 |
5 files changed, 548 insertions, 0 deletions
diff --git a/tests/sys/common/Makefile b/tests/sys/common/Makefile new file mode 100644 index 000000000000..269aa9d52f9a --- /dev/null +++ b/tests/sys/common/Makefile @@ -0,0 +1,12 @@ +PACKAGE= tests +TESTSDIR= ${TESTSBASE}/sys/common +${PACKAGE}FILES+= vnet.subr +${PACKAGE}FILES+= divert.py +${PACKAGE}FILES+= sender.py +${PACKAGE}FILES+= net_receiver.py + +${PACKAGE}FILESMODE_divert.py=0555 +${PACKAGE}FILESMODE_sender.py=0555 +${PACKAGE}FILESMODE_net_receiver.py=0555 + +.include <bsd.test.mk> diff --git a/tests/sys/common/divert.py b/tests/sys/common/divert.py new file mode 100755 index 000000000000..830b61a585a0 --- /dev/null +++ b/tests/sys/common/divert.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# - +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2020 Alexander V. Chernikov +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# + + +from socket import socket, PF_DIVERT, SOCK_RAW +import logging +logging.getLogger("scapy").setLevel(logging.CRITICAL) +import scapy.all as sc +import argparse + + +def parse_args(): + parser = argparse.ArgumentParser(description='divert socket tester') + parser.add_argument('--dip', type=str, help='destination packet IP') + parser.add_argument('--sip', type=str, help='source packet IP') + parser.add_argument('--divert_port', type=int, default=6668, + help='divert port to use') + parser.add_argument('--test_name', type=str, required=True, + help='test name to run') + return parser.parse_args() + + +def ipdivert_ip_output_remote_success(args): + packet = sc.IP(dst=args.dip) / sc.ICMP(type='echo-request') + with socket(PF_DIVERT, SOCK_RAW, 0) as s: + s.bind(('0.0.0.0', args.divert_port)) + s.sendto(bytes(packet), ('0.0.0.0', 0)) + + +def ipdivert_ip6_output_remote_success(args): + packet = sc.IPv6(dst=args.dip) / sc.ICMPv6EchoRequest() + with socket(PF_DIVERT, SOCK_RAW, 0) as s: + s.bind(('0.0.0.0', args.divert_port)) + s.sendto(bytes(packet), ('0.0.0.0', 0)) + + +def ipdivert_ip_input_local_success(args): + """Sends IPv4 packet to OS stack as inbound local packet.""" + packet = sc.IP(dst=args.dip,src=args.sip) / sc.ICMP(type='echo-request') + with socket(PF_DIVERT, SOCK_RAW, 0) as s: + s.bind(('0.0.0.0', args.divert_port)) + s.sendto(bytes(packet), (args.dip, 0)) + + +# XXX: IPv6 local divert is currently not supported +# TODO: add IPv4 ifname output verification + + +def main(): + args = parse_args() + test_ptr = globals()[args.test_name] + test_ptr(args) + + +if __name__ == '__main__': + main() diff --git a/tests/sys/common/net_receiver.py b/tests/sys/common/net_receiver.py new file mode 100755 index 000000000000..63c8a6927fd2 --- /dev/null +++ b/tests/sys/common/net_receiver.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# - +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2020 Alexander V. Chernikov +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# + + +from functools import partial +import socket +import select +import argparse +import time + + +def parse_args(): + parser = argparse.ArgumentParser(description='divert socket tester') + parser.add_argument('--sip', type=str, default='', help='IP to listen on') + parser.add_argument('--family', type=str, help='inet/inet6') + parser.add_argument('--ports', type=str, help='packet ports 1,2,3') + parser.add_argument('--match_str', type=str, help='match string to use') + parser.add_argument('--count', type=int, default=1, + help='Number of messages to receive') + parser.add_argument('--test_name', type=str, required=True, + help='test name to run') + return parser.parse_args() + + +def test_listen_tcp(args): + if args.family == 'inet6': + fam = socket.AF_INET6 + else: + fam = socket.AF_INET + sockets = [] + ports = [int(port) for port in args.ports.split(',')] + for port in ports: + s = socket.socket(fam, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.setblocking(0) + s.bind((args.sip, port)) + print('binding on {}:{}'.format(args.sip, port)) + s.listen(5) + sockets.append(s) + inputs = sockets + count = 0 + while count < args.count: + readable, writable, exceptional = select.select(inputs, [], inputs) + for s in readable: + (c, address) = s.accept() + print('C: {}'.format(address)) + data = c.recv(9000) + if args.match_str and args.match_str.encode('utf-8') != data: + raise Exception('Expected "{}" but got "{}"'.format( + args.match_str, data.decode('utf-8'))) + count += 1 + c.close() + + +def test_listen_udp(args): + if args.family == 'inet6': + fam = socket.AF_INET6 + else: + fam = socket.AF_INET + sockets = [] + ports = [int(port) for port in args.ports.split(',')] + for port in ports: + s = socket.socket(fam, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.setblocking(0) + s.bind((args.sip, port)) + print('binding on {}:{}'.format(args.sip, port)) + sockets.append(s) + inputs = sockets + count = 0 + while count < args.count: + readable, writable, exceptional = select.select(inputs, [], inputs) + for s in readable: + (data, address) = s.recvfrom(9000) + print('C: {}'.format(address)) + if args.match_str and args.match_str.encode('utf-8') != data: + raise Exception('Expected "{}" but got "{}"'.format( + args.match_str, data.decode('utf-8'))) + count += 1 + + +def main(): + args = parse_args() + test_ptr = globals()[args.test_name] + test_ptr(args) + + +if __name__ == '__main__': + main() diff --git a/tests/sys/common/sender.py b/tests/sys/common/sender.py new file mode 100755 index 000000000000..b2a64371e8bb --- /dev/null +++ b/tests/sys/common/sender.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# - +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2020 Alexander V. Chernikov +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# + + +from functools import partial +import socket +import logging +logging.getLogger("scapy").setLevel(logging.CRITICAL) +import scapy.all as sc +import argparse +import time + + +def parse_args(): + parser = argparse.ArgumentParser(description='divert socket tester') + parser.add_argument('--dip', type=str, help='destination packet IP') + parser.add_argument('--sip', type=str, help='source packet IP') + parser.add_argument('--dmac', type=str, help='packet dst mac') + parser.add_argument('--smac', type=str, help='packet src mac') + parser.add_argument('--iface', type=str, help='interface to use') + parser.add_argument('--test_name', type=str, required=True, + help='test name to run') + return parser.parse_args() + + +def send_packet(args, pkt): + sc.sendp(pkt, iface=args.iface, verbose=False) + + +def is_icmp6_echo_request(pkt): + return pkt.type == 0x86DD and pkt.payload.nh == 58 and \ + pkt.payload.payload.type == 128 + + +def check_forwarded_ip_packet(orig_pkt, fwd_pkt): + """ + Checks that forwarded ICMP packet @fwd_ptk is the same as + @orig_pkt. Assumes router-on-the-stick forwarding behaviour: + * src/dst macs are swapped + * TTL is decremented + """ + # Check ether fields + assert orig_pkt.src == fwd_pkt.dst + assert orig_pkt.dst == fwd_pkt.src + assert len(orig_pkt) == len(fwd_pkt) + # Check IP + fwd_ip = fwd_pkt[sc.IP] + orig_ip = orig_pkt[sc.IP] + assert orig_ip.src == orig_ip.src + assert orig_ip.dst == fwd_ip.dst + assert orig_ip.ttl == fwd_ip.ttl + 1 + # Check ICMP + fwd_icmp = fwd_ip[sc.ICMP] + orig_icmp = orig_ip[sc.ICMP] + assert bytes(orig_ip.payload) == bytes(fwd_ip.payload) + + +def fwd_ip_icmp_fast(args): + """ + Sends ICMP packet via args.iface interface. + Receives and checks the forwarded packet. + Assumes forwarding router decrements TTL + """ + + def filter_f(x): + return x.src == args.dmac and x.type == 0x0800 + + e = sc.Ether(src=args.smac, dst=args.dmac) + ip = sc.IP(src=args.sip, dst=args.dip) + icmp = sc.ICMP(type='echo-request') + pkt = e / ip / icmp + + send_cb = partial(send_packet, args, pkt) + packets = sc.sniff(iface=args.iface, started_callback=send_cb, + stop_filter=filter_f, lfilter=filter_f, timeout=5) + assert len(packets) > 0 + fwd_pkt = packets[-1] + try: + check_forwarded_ip_packet(pkt, fwd_pkt) + except Exception as e: + print('Original packet:') + pkt.show() + print('Forwarded packet:') + fwd_pkt.show() + for a_packet in packets: + a_packet.summary() + raise Exception from e + + +def fwd_ip_icmp_slow(args): + """ + Sends ICMP packet via args.iface interface. + Forces slow path processing by introducing IP option. + Receives and checks the forwarded packet. + Assumes forwarding router decrements TTL + """ + + def filter_f(x): + return x.src == args.dmac and x.type == 0x0800 + + e = sc.Ether(src=args.smac, dst=args.dmac) + # Add IP option to switch to 'normal' IP processing + stream_id = sc.IPOption_Stream_Id(security=0xFFFF) + ip = sc.IP(src=args.sip, dst=args.dip, + options=[sc.IPOption_Stream_Id(security=0xFFFF)]) + icmp = sc.ICMP(type='echo-request') + pkt = e / ip / icmp + + send_cb = partial(send_packet, args, pkt) + packets = sc.sniff(iface=args.iface, started_callback=send_cb, + stop_filter=filter_f, lfilter=filter_f, timeout=5) + assert len(packets) > 0 + check_forwarded_ip_packet(pkt, packets[-1]) + + +def check_forwarded_ip6_packet(orig_pkt, fwd_pkt): + """ + Checks that forwarded ICMP packet @fwd_ptk is the same as + @orig_pkt. Assumes router-on-the-stick forwarding behaviour: + * src/dst macs are swapped + * TTL is decremented + """ + # Check ether fields + assert orig_pkt.src == fwd_pkt.dst + assert orig_pkt.dst == fwd_pkt.src + assert len(orig_pkt) == len(fwd_pkt) + # Check IP + fwd_ip = fwd_pkt[sc.IPv6] + orig_ip = orig_pkt[sc.IPv6] + assert orig_ip.src == orig_ip.src + assert orig_ip.dst == fwd_ip.dst + assert orig_ip.hlim == fwd_ip.hlim + 1 + # Check ICMPv6 + assert bytes(orig_ip.payload) == bytes(fwd_ip.payload) + + +def fwd_ip6_icmp(args): + """ + Sends ICMPv6 packet via args.iface interface. + Receives and checks the forwarded packet. + Assumes forwarding router decrements TTL + """ + + def filter_f(x): + return x.src == args.dmac and is_icmp6_echo_request(x) + + e = sc.Ether(src=args.smac, dst=args.dmac) + ip = sc.IPv6(src=args.sip, dst=args.dip) + icmp = sc.ICMPv6EchoRequest() + pkt = e / ip / icmp + + send_cb = partial(send_packet, args, pkt) + packets = sc.sniff(iface=args.iface, started_callback=send_cb, + stop_filter=filter_f, lfilter=filter_f, timeout=5) + assert len(packets) > 0 + fwd_pkt = packets[-1] + try: + check_forwarded_ip6_packet(pkt, fwd_pkt) + except Exception as e: + print('Original packet:') + pkt.show() + print('Forwarded packet:') + fwd_pkt.show() + for idx, a_packet in enumerate(packets): + print('{}: {}'.format(idx, a_packet.summary())) + raise Exception from e + + +def main(): + args = parse_args() + test_ptr = globals()[args.test_name] + test_ptr(args) + + +if __name__ == '__main__': + main() diff --git a/tests/sys/common/vnet.subr b/tests/sys/common/vnet.subr new file mode 100644 index 000000000000..0a32e6caf813 --- /dev/null +++ b/tests/sys/common/vnet.subr @@ -0,0 +1,137 @@ +# VNET/jail utility functions +## + +list_interface() +{ + echo $1 >> created_interfaces.lst +} + +unlist_interface() +{ + sed -i "" /^$1\$/d created_interfaces.lst +} + +_vnet_check_req() +{ + type=$1 + + if kldstat -q -n if_${type}.ko; then + return + fi + + if ! kldload -n -q if_${type}; then + atf_skip "if_${type}.ko is required to run this test." + return + fi +} + +vnet_init() +{ + if [ "`sysctl -i -n kern.features.vimage`" != 1 ]; then + atf_skip "This test requires VIMAGE" + fi + + # Check if we can create if_epair or if_bridge interfaces. + # We may be running in a jail already, unable to load modules. + # If so, skip this test because it very likely (but not certainly) + # wants at least one of those + _vnet_check_req epair + _vnet_check_req bridge +} + +vnet_mkepair() +{ + ifname=$(ifconfig epair create) + # When transmit checksum offloading is enabled, if_epair does not + # compute checksums, it just marks packets that this computation still + # needs to be done. However, some test cases verify the checksum. + # Therefore disable this for IPv4 and IPv6. + ifconfig ${ifname} -txcsum -txcsum6 + list_interface $ifname + list_interface ${ifname%a}b + echo ${ifname%a} +} + +vnet_init_bridge() +{ + if ! kldstat -q -m if_bridge; then + atf_skip "This test requires if_bridge" + fi +} + +vnet_mkbridge() +{ + ifname=$(ifconfig bridge create) + list_interface $ifname + echo ${ifname} +} + +vnet_mkvlan() +{ + ifname=$(ifconfig vlan create) + list_interface $ifname + echo ${ifname} +} + +vnet_mkloopback() +{ + ifname=$(ifconfig lo create) + list_interface $ifname + echo ${ifname} +} + +vnet_mkjail() +{ + jailname=$1 + shift + + vnet_interfaces= + for ifname in $@ + do + vnet_interfaces="${vnet_interfaces} vnet.interface=${ifname}" + unlist_interface $ifname + done + jail -c name=${jailname} persist vnet ${vnet_interfaces} + + echo $jailname $@ >> created_jails.lst +} + +vnet_ifmove() +{ + ifname=$1 + jailname=$2 + + ifconfig ${ifname} vnet ${jailname} + unlist_interface $ifname + sed -i "" "/^${jailname}/s/\$/ ${ifname}/" created_jails.lst +} + +vnet_ifrename_jail() +{ + jailname=$1 + ifname=$2 + ifnewname=$3 + + ifconfig -j ${jailname} $ifname name $ifnewname + sed -i "" "/^${jailname}/s/${ifname}/${ifnewname}/" created_jails.lst +} + +vnet_cleanup() +{ + if [ -f created_jails.lst ]; then + while read jailname ifnames; do + for ifname in ${ifnames}; do + ifconfig -j ${jailname} ${ifname} destroy + done + jail -r ${jailname} + done < created_jails.lst + rm created_jails.lst + fi + + if [ -f created_interfaces.lst ]; then + while read ifname; do + ifconfig ${ifname} destroy + done < created_interfaces.lst + rm created_interfaces.lst + fi +} |
