diff options
Diffstat (limited to 'tests/sys/netpfil')
77 files changed, 8759 insertions, 702 deletions
diff --git a/tests/sys/netpfil/Makefile b/tests/sys/netpfil/Makefile index bdbb9078cfc2..b449902aabc2 100644 --- a/tests/sys/netpfil/Makefile +++ b/tests/sys/netpfil/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> TESTSDIR= ${TESTSBASE}/sys/netpfil diff --git a/tests/sys/netpfil/common/Makefile b/tests/sys/netpfil/common/Makefile index 0003aac28779..0938bd9d9c7e 100644 --- a/tests/sys/netpfil/common/Makefile +++ b/tests/sys/netpfil/common/Makefile @@ -1,7 +1,7 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netpfil/common +BINDIR= ${TESTSDIR} ATF_TESTS_SH+= \ @@ -13,8 +13,11 @@ ATF_TESTS_SH+= \ fragments \ forward -# Tests reuse jail names and so cannot run in parallel. -TEST_METADATA+= is_exclusive=true +# Allow tests to run in parallel in their own jails +TEST_METADATA+= execenv="jail" +TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets" + +PROGS= divapp ${PACKAGE}FILES+= \ utils.subr \ diff --git a/tests/sys/netpfil/pf/divapp.c b/tests/sys/netpfil/common/divapp.c index 908c41eaa67f..d0f4b345b14c 100644 --- a/tests/sys/netpfil/pf/divapp.c +++ b/tests/sys/netpfil/common/divapp.c @@ -25,7 +25,7 @@ * SUCH DAMAGE. */ -/* Used by tests like divert-to.sh */ +/* Used by divert(4) related tests */ #include <errno.h> #include <stdlib.h> @@ -83,8 +83,8 @@ recv_pkt(struct context *c) s = select(c->fd + 1, &readfds, 0, 0, &timeout); if (s == -1) errx(EX_IOERR, "recv_pkt: select() errors."); - if (s != 1) // timeout - return -1; + if (s != 1) /* timeout */ + return (-1); c->pkt_n = recvfrom(c->fd, c->pkt, sizeof(c->pkt), 0, (struct sockaddr *) &c->sin, &c->sin_len); @@ -98,14 +98,11 @@ static void send_pkt(struct context *c) { ssize_t n; - char errstr[32]; n = sendto(c->fd, c->pkt, c->pkt_n, 0, (struct sockaddr *) &c->sin, c->sin_len); - if (n == -1) { - strerror_r(errno, errstr, sizeof(errstr)); - errx(EX_IOERR, "send_pkt: sendto() errors: %d %s.", errno, errstr); - } + if (n == -1) + err(EX_IOERR, "send_pkt: sendto() errors"); if (n != c->pkt_n) errx(EX_IOERR, "send_pkt: sendto() sent %zd of %zd bytes.", n, c->pkt_n); @@ -145,5 +142,5 @@ main(int argc, char *argv[]) if (npkt != 1) errx(EXIT_FAILURE, "%d: npkt=%d.", c.divert_port, npkt); - return EXIT_SUCCESS; + return (EXIT_SUCCESS); } diff --git a/tests/sys/netpfil/common/dummynet.sh b/tests/sys/netpfil/common/dummynet.sh index 3c930cfe2aff..b77b2df84010 100644 --- a/tests/sys/netpfil/common/dummynet.sh +++ b/tests/sys/netpfil/common/dummynet.sh @@ -277,7 +277,7 @@ queue_body() ifconfig ${epair}a 192.0.2.1/24 up jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/../pf/echo_inetd.conf # Sanity check @@ -320,7 +320,7 @@ queue_body() # TCP should still just pass fails=0 - for i in `seq 1 3` + for i in `seq 1 5` do result=$(dd if=/dev/zero bs=1024 count=2000 | timeout 3 nc -w 5 -N 192.0.2.2 7 | wc -c) if [ $result -ne 2048000 ]; @@ -329,7 +329,7 @@ queue_body() fails=$(( ${fails} + 1 )) fi done - if [ ${fails} -gt 0 ]; + if [ ${fails} -gt 2 ]; then atf_fail "We failed prioritisation ${fails} times" fi @@ -348,7 +348,7 @@ queue_body() sleep 1 fails=0 - for i in `seq 1 3` + for i in `seq 1 5` do result=$(dd if=/dev/zero bs=1024 count=2000 | timeout 3 nc -w 5 -N 192.0.2.2 7 | wc -c) if [ $result -ne 2048000 ]; @@ -385,7 +385,7 @@ queue_v6_body() ifconfig ${epair}a inet6 2001:db8:42::1/64 no_dad up jexec alcatraz ifconfig ${epair}b inet6 2001:db8:42::2 no_dad up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/../pf/echo_inetd.conf jexec alcatraz sysctl net.inet6.icmp6.errppslimit=0 @@ -429,7 +429,7 @@ queue_v6_body() # TCP should still just pass fails=0 - for i in `seq 1 3` + for i in `seq 1 5` do result=$(dd if=/dev/zero bs=1024 count=1000 | timeout 3 nc -w 5 -N 2001:db8:42::2 7 | wc -c) if [ $result -ne 1024000 ]; @@ -438,7 +438,7 @@ queue_v6_body() fails=$(( ${fails} + 1 )) fi done - if [ ${fails} -gt 0 ]; + if [ ${fails} -gt 2 ]; then atf_fail "We failed prioritisation ${fails} times" fi @@ -454,7 +454,7 @@ queue_v6_body() "pass in proto icmp6 dnqueue (0, 100)" fails=0 - for i in `seq 1 3` + for i in `seq 1 5` do result=$(dd if=/dev/zero bs=1024 count=1000 | timeout 3 nc -w 5 -N 2001:db8:42::2 7 | wc -c) if [ $result -ne 1024000 ]; @@ -557,7 +557,7 @@ pls_basic_body() # are dropped (84 - 96 responses). # repeat up to 6 times if the initial # checks fail - atf_check -s exit:0 -o match:'100 packets transmitted, (8[4-9]|9[0-6]) packets received' -r 6:10 ping -i 0.010 -c 100 192.0.2.2 + atf_check -s exit:0 -o match:'100 packets transmitted, (8[4-9]|9[0-6]) packets received' -r 20:10 ping -i 0.010 -c 100 192.0.2.2 } pls_basic_cleanup() @@ -604,7 +604,7 @@ pls_gilbert_body() # are dropped (70 - 85 responses). # repeat up to 6 times if the initial # checks fail - atf_check -s exit:0 -o match:'100 packets transmitted, (7[0-9]|8[0-5]) packets received' -r 6:10 ping -i 0.010 -c 100 192.0.2.2 + atf_check -s exit:0 -o match:'100 packets transmitted, (7[0-9]|8[0-5]) packets received' -r 20:10 ping -i 0.010 -c 100 192.0.2.2 } pls_gilbert_cleanup() diff --git a/tests/sys/netpfil/common/forward.sh b/tests/sys/netpfil/common/forward.sh index 939ce55f2d33..fa1f97aa0390 100644 --- a/tests/sys/netpfil/common/forward.sh +++ b/tests/sys/netpfil/common/forward.sh @@ -33,7 +33,7 @@ v4_head() { atf_set descr 'Basic forwarding test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v4_body() diff --git a/tests/sys/netpfil/common/pft_ping.py b/tests/sys/netpfil/common/pft_ping.py index a24a1e00150a..a2a1d9c7f4ec 100644 --- a/tests/sys/netpfil/common/pft_ping.py +++ b/tests/sys/netpfil/common/pft_ping.py @@ -33,6 +33,7 @@ logging.getLogger("scapy").setLevel(logging.CRITICAL) import math import scapy.all as sp import sys +import socket from copy import copy from sniffer import Sniffer @@ -49,8 +50,17 @@ def build_payload(l): return ret -def prepare_ipv6(dst_address, send_params): +def clean_params(params): + # Prepare a copy of safe copy of params + ret = copy(params) + ret.pop('src_address') + ret.pop('dst_address') + ret.pop('flags') + return ret + +def prepare_ipv6(send_params): src_address = send_params.get('src_address') + dst_address = send_params.get('dst_address') hlim = send_params.get('hlim') tc = send_params.get('tc') ip6 = sp.IPv6(dst=dst_address) @@ -63,8 +73,9 @@ def prepare_ipv6(dst_address, send_params): return ip6 -def prepare_ipv4(dst_address, send_params): +def prepare_ipv4(send_params): src_address = send_params.get('src_address') + dst_address = send_params.get('dst_address') flags = send_params.get('flags') tos = send_params.get('tc') ttl = send_params.get('hlim') @@ -84,22 +95,22 @@ def prepare_ipv4(dst_address, send_params): return ip -def send_icmp_ping(dst_address, sendif, send_params): +def send_icmp_ping(send_params): send_length = send_params['length'] send_frag_length = send_params['frag_length'] packets = [] ether = sp.Ether() - if ':' in dst_address: - ip6 = prepare_ipv6(dst_address, send_params) + if ':' in send_params['dst_address']: + ip6 = prepare_ipv6(send_params) icmp = sp.ICMPv6EchoRequest(data=sp.raw(build_payload(send_length))) if send_frag_length: - for packet in sp.fragment(ip6 / icmp, fragsize=send_frag_length): + for packet in sp.fragment6(ip6 / icmp, fragSize=send_frag_length): packets.append(ether / packet) else: packets.append(ether / ip6 / icmp) else: - ip = prepare_ipv4(dst_address, send_params) + ip = prepare_ipv4(send_params) icmp = sp.ICMP(type='echo-request') raw = sp.raw(build_payload(send_length)) if send_frag_length: @@ -108,10 +119,10 @@ def send_icmp_ping(dst_address, sendif, send_params): else: packets.append(ether / ip / icmp / raw) for packet in packets: - sp.sendp(packet, sendif, verbose=False) + sp.sendp(packet, iface=send_params['sendif'], verbose=False) -def send_tcp_syn(dst_address, sendif, send_params): +def send_tcp_syn(send_params): tcpopt_unaligned = send_params.get('tcpopt_unaligned') seq = send_params.get('seq') mss = send_params.get('mss') @@ -119,22 +130,63 @@ def send_tcp_syn(dst_address, sendif, send_params): opts=[('Timestamp', (1, 1)), ('MSS', mss if mss else 1280)] if tcpopt_unaligned: opts = [('NOP', 0 )] + opts - if ':' in dst_address: - ip = prepare_ipv6(dst_address, send_params) + if ':' in send_params['dst_address']: + ip = prepare_ipv6(send_params) else: - ip = prepare_ipv4(dst_address, send_params) - tcp = sp.TCP(dport=666, flags='S', options=opts, seq=seq) + ip = prepare_ipv4(send_params) + tcp = sp.TCP( + sport=send_params.get('sport'), dport=send_params.get('dport'), + flags='S', options=opts, seq=seq, + ) req = ether / ip / tcp - sp.sendp(req, iface=sendif, verbose=False) + sp.sendp(req, iface=send_params['sendif'], verbose=False) + + +def send_udp(send_params): + LOGGER.debug(f'Sending UDP ping') + packets = [] + send_length = send_params['length'] + send_frag_length = send_params['frag_length'] + ether = sp.Ether() + if ':' in send_params['dst_address']: + ip6 = prepare_ipv6(send_params) + udp = sp.UDP( + sport=send_params.get('sport'), dport=send_params.get('dport'), + ) + raw = sp.Raw(load=build_payload(send_length)) + if send_frag_length: + for packet in sp.fragment6(ip6 / udp / raw, fragSize=send_frag_length): + packets.append(ether / packet) + else: + packets.append(ether / ip6 / udp / raw) + else: + ip = prepare_ipv4(send_params) + udp = sp.UDP( + sport=send_params.get('sport'), dport=send_params.get('dport'), + ) + raw = sp.Raw(load=build_payload(send_length)) + if send_frag_length: + for packet in sp.fragment(ip / udp / raw, fragsize=send_frag_length): + packets.append(ether / packet) + else: + packets.append(ether / ip / udp / raw) + + for packet in packets: + sp.sendp(packet, iface=send_params['sendif'], verbose=False) -def send_ping(dst_address, sendif, ping_type, send_params): +def send_ping(ping_type, send_params): if ping_type == 'icmp': - send_icmp_ping(dst_address, sendif, send_params) - elif ping_type == 'tcpsyn': - send_tcp_syn(dst_address, sendif, send_params) + send_icmp_ping(send_params) + elif ( + ping_type == 'tcpsyn' or + ping_type == 'tcp3way' + ): + send_tcp_syn(send_params) + elif ping_type == 'udp': + send_udp(send_params) else: - raise Exception('Unspported ping type') + raise Exception('Unsupported ping type') def check_ipv4(expect_params, packet): @@ -144,20 +196,15 @@ def check_ipv4(expect_params, packet): tos = expect_params.get('tc') ttl = expect_params.get('hlim') ip = packet.getlayer(sp.IP) + LOGGER.debug(f'Packet: {ip}') if not ip: LOGGER.debug('Packet is not IPv4!') return False if src_address and ip.src != src_address: - LOGGER.debug('Source IPv4 address does not match!') + LOGGER.debug(f'Wrong IPv4 source {ip.src}, expected {src_address}') return False if dst_address and ip.dst != dst_address: - LOGGER.debug('Destination IPv4 address does not match!') - return False - chksum = ip.chksum - ip.chksum = None - new_chksum = sp.IP(sp.raw(ip)).chksum - if chksum != new_chksum: - LOGGER.debug(f'Expected IP checksum {new_chksum} but found {chksum}') + LOGGER.debug(f'Wrong IPv4 destination {ip.dst}, expected {dst_address}') return False if flags and ip.flags != flags: LOGGER.debug(f'Wrong IP flags value {ip.flags}, expected {flags}') @@ -181,11 +228,13 @@ def check_ipv6(expect_params, packet): if not ip6: LOGGER.debug('Packet is not IPv6!') return False - if src_address and ip6.src != src_address: - LOGGER.debug('Source IPv6 address does not match!') + if src_address and socket.inet_pton(socket.AF_INET6, ip6.src) != \ + socket.inet_pton(socket.AF_INET6, src_address): + LOGGER.debug(f'Wrong IPv6 source {ip6.src}, expected {src_address}') return False - if dst_address and ip6.dst != dst_address: - LOGGER.debug('Destination IPv6 address does not match!') + if dst_address and socket.inet_pton(socket.AF_INET6, ip6.dst) != \ + socket.inet_pton(socket.AF_INET6, dst_address): + LOGGER.debug(f'Wrong IPv6 destination {ip6.dst}, expected {dst_address}') return False # IPv6 has no IP-level checksum. if flags: @@ -265,32 +314,32 @@ def check_ping_reply_6(expect_params, packet): return True -def check_ping_request(expect_params, packet): - src_address = expect_params.get('src_address') - dst_address = expect_params.get('dst_address') +def check_ping_request(args, packet): + src_address = args['expect_params'].get('src_address') + dst_address = args['expect_params'].get('dst_address') if not (src_address or dst_address): raise Exception('Source or destination address must be given to match the ping request!') if ( (src_address and ':' in src_address) or (dst_address and ':' in dst_address) ): - return check_ping_request_6(expect_params, packet) + return check_ping_request_6(args['expect_params'], packet) else: - return check_ping_request_4(expect_params, packet) + return check_ping_request_4(args['expect_params'], packet) -def check_ping_reply(expect_params, packet): - src_address = expect_params.get('src_address') - dst_address = expect_params.get('dst_address') +def check_ping_reply(args, packet): + src_address = args['expect_params'].get('src_address') + dst_address = args['expect_params'].get('dst_address') if not (src_address or dst_address): raise Exception('Source or destination address must be given to match the ping reply!') if ( (src_address and ':' in src_address) or (dst_address and ':' in dst_address) ): - return check_ping_reply_6(expect_params, packet) + return check_ping_reply_6(args['expect_params'], packet) else: - return check_ping_reply_4(expect_params, packet) + return check_ping_reply_4(args['expect_params'], packet) def check_tcp(expect_params, packet): @@ -305,7 +354,7 @@ def check_tcp(expect_params, packet): tcp.chksum = None newpacket = sp.Ether(sp.raw(packet[sp.Ether])) new_chksum = newpacket[sp.TCP].chksum - if chksum != new_chksum: + if new_chksum and chksum != new_chksum: LOGGER.debug(f'Wrong TCP checksum {chksum}, expected {new_chksum}!') return False if tcp_flags and tcp.flags != tcp_flags: @@ -328,6 +377,30 @@ def check_tcp(expect_params, packet): return True +def check_udp(expect_params, packet): + expect_length = expect_params['length'] + udp = packet.getlayer(sp.UDP) + if not udp: + LOGGER.debug('Packet is not UDP!') + return False + raw = packet.getlayer(sp.Raw) + if not raw: + LOGGER.debug('Packet contains no payload!') + return False + if raw.load != build_payload(expect_length): + LOGGER.debug(f'Payload magic does not match len {len(raw.load)} vs {expect_length}!') + return False + orig_chksum = udp.chksum + udp.chksum = None + newpacket = sp.Ether(sp.raw(packet[sp.Ether])) + new_chksum = newpacket[sp.UDP].chksum + if new_chksum and orig_chksum != new_chksum: + LOGGER.debug(f'Wrong UDP checksum {orig_chksum}, expected {new_chksum}!') + return False + + return True + + def check_tcp_syn_request_4(expect_params, packet): if not check_ipv4(expect_params, packet): return False @@ -336,7 +409,7 @@ def check_tcp_syn_request_4(expect_params, packet): return True -def check_tcp_syn_reply_4(expect_params, packet): +def check_tcp_syn_reply_4(send_params, expect_params, packet): if not check_ipv4(expect_params, packet): return False if not check_tcp(expect_params | {'tcp_flags': 'SA'}, packet): @@ -344,6 +417,44 @@ def check_tcp_syn_reply_4(expect_params, packet): return True +def check_tcp_3way_4(args, packet): + send_params = args['send_params'] + + expect_params_sa = clean_params(args['expect_params']) + expect_params_sa['src_address'] = send_params['dst_address'] + expect_params_sa['dst_address'] = send_params['src_address'] + + # Sniff incoming SYN+ACK packet + if ( + check_ipv4(expect_params_sa, packet) and + check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet) + ): + ether = sp.Ether() + ip_sa = packet.getlayer(sp.IP) + tcp_sa = packet.getlayer(sp.TCP) + reply_params = clean_params(send_params) + reply_params['src_address'] = ip_sa.dst + reply_params['dst_address'] = ip_sa.src + ip_a = prepare_ipv4(reply_params) + tcp_a = sp.TCP( + sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A', + seq=tcp_sa.ack, ack=tcp_sa.seq + 1, + ) + req = ether / ip_a / tcp_a + sp.sendp(req, iface=send_params['sendif'], verbose=False) + return True + + return False + + +def check_udp_request_4(expect_params, packet): + if not check_ipv4(expect_params, packet): + return False + if not check_udp(expect_params, packet): + return False + return True + + def check_tcp_syn_request_6(expect_params, packet): if not check_ipv6(expect_params, packet): return False @@ -360,7 +471,45 @@ def check_tcp_syn_reply_6(expect_params, packet): return True -def check_tcp_syn_request(expect_params, packet): +def check_tcp_3way_6(args, packet): + send_params = args['send_params'] + + expect_params_sa = clean_params(args['expect_params']) + expect_params_sa['src_address'] = send_params['dst_address'] + expect_params_sa['dst_address'] = send_params['src_address'] + + # Sniff incoming SYN+ACK packet + if ( + check_ipv6(expect_params_sa, packet) and + check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet) + ): + ether = sp.Ether() + ip6_sa = packet.getlayer(sp.IPv6) + tcp_sa = packet.getlayer(sp.TCP) + reply_params = clean_params(send_params) + reply_params['src_address'] = ip6_sa.dst + reply_params['dst_address'] = ip6_sa.src + ip_a = prepare_ipv6(reply_params) + tcp_a = sp.TCP( + sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A', + seq=tcp_sa.ack, ack=tcp_sa.seq + 1, + ) + req = ether / ip_a / tcp_a + sp.sendp(req, iface=send_params['sendif'], verbose=False) + return True + + return False + + +def check_udp_request_6(expect_params, packet): + if not check_ipv6(expect_params, packet): + return False + if not check_udp(expect_params, packet): + return False + return True + +def check_tcp_syn_request(args, packet): + expect_params = args['expect_params'] src_address = expect_params.get('src_address') dst_address = expect_params.get('dst_address') if not (src_address or dst_address): @@ -374,7 +523,8 @@ def check_tcp_syn_request(expect_params, packet): return check_tcp_syn_request_4(expect_params, packet) -def check_tcp_syn_reply(expect_params, packet): +def check_tcp_syn_reply(args, packet): + expect_params = args['expect_params'] src_address = expect_params.get('src_address') dst_address = expect_params.get('dst_address') if not (src_address or dst_address): @@ -387,8 +537,39 @@ def check_tcp_syn_reply(expect_params, packet): else: return check_tcp_syn_reply_4(expect_params, packet) +def check_tcp_3way(args, packet): + expect_params = args['expect_params'] + src_address = expect_params.get('src_address') + dst_address = expect_params.get('dst_address') + if not (src_address or dst_address): + raise Exception('Source or destination address must be given to match the tcp syn reply!') + if ( + (src_address and ':' in src_address) or + (dst_address and ':' in dst_address) + ): + return check_tcp_3way_6(args, packet) + else: + return check_tcp_3way_4(args, packet) + -def setup_sniffer(recvif, ping_type, sniff_type, expect_params, defrag): +def check_udp_request(args, packet): + expect_params = args['expect_params'] + src_address = expect_params.get('src_address') + dst_address = expect_params.get('dst_address') + if not (src_address or dst_address): + raise Exception('Source or destination address must be given to match the tcp syn request!') + if ( + (src_address and ':' in src_address) or + (dst_address and ':' in dst_address) + ): + return check_udp_request_6(expect_params, packet) + else: + return check_udp_request_4(expect_params, packet) + + +def setup_sniffer( + recvif, ping_type, sniff_type, expect_params, defrag, send_params, +): if ping_type == 'icmp' and sniff_type == 'request': checkfn = check_ping_request elif ping_type == 'icmp' and sniff_type == 'reply': @@ -397,10 +578,17 @@ def setup_sniffer(recvif, ping_type, sniff_type, expect_params, defrag): checkfn = check_tcp_syn_request elif ping_type == 'tcpsyn' and sniff_type == 'reply': checkfn = check_tcp_syn_reply + elif ping_type == 'tcp3way' and sniff_type == 'reply': + checkfn = check_tcp_3way + elif ping_type == 'udp' and sniff_type == 'request': + checkfn = check_udp_request else: - raise Exception('Unspported ping or sniff type') + raise Exception('Unspported ping and sniff type combination') - return Sniffer(expect_params, checkfn, recvif, defrag=defrag) + return Sniffer( + {'send_params': send_params, 'expect_params': expect_params}, + checkfn, recvif, defrag=defrag, + ) def parse_args(): @@ -408,17 +596,15 @@ def parse_args(): description="Ping test tool") # Parameters of sent ping request - parser.add_argument('--sendif', nargs=1, - required=True, + parser.add_argument('--sendif', required=True, help='The interface through which the packet(s) will be sent') - parser.add_argument('--to', nargs=1, - required=True, + parser.add_argument('--to', required=True, help='The destination IP address for the ping request') parser.add_argument('--ping-type', - choices=('icmp', 'tcpsyn'), - help='Type of ping: ICMP (default) or TCP SYN', + choices=('icmp', 'tcpsyn', 'tcp3way', 'udp'), + help='Type of ping: ICMP (default) or TCP SYN or 3-way TCP handshake', default='icmp') - parser.add_argument('--fromaddr', nargs=1, + parser.add_argument('--fromaddr', help='The source IP address for the ping request') # Where to look for packets to analyze. @@ -431,36 +617,40 @@ def parse_args(): # Packet settings parser_send = parser.add_argument_group('Values set in transmitted packets') - parser_send.add_argument('--send-flags', nargs=1, type=str, + parser_send.add_argument('--send-flags', type=str, help='IPv4 fragmentation flags') - parser_send.add_argument('--send-frag-length', nargs=1, type=int, - help='Force IP fragmentation with given fragment length') - parser_send.add_argument('--send-hlim', nargs=1, type=int, + parser_send.add_argument('--send-frag-length', type=int, + help='Force IP fragmentation with given fragment length') + parser_send.add_argument('--send-hlim', type=int, help='IPv6 Hop Limit or IPv4 Time To Live') - parser_send.add_argument('--send-mss', nargs=1, type=int, + parser_send.add_argument('--send-mss', type=int, help='TCP Maximum Segment Size') - parser_send.add_argument('--send-seq', nargs=1, type=int, + parser_send.add_argument('--send-seq', type=int, help='TCP sequence number') - parser_send.add_argument('--send-length', nargs=1, type=int, - default=[len(PAYLOAD_MAGIC)], help='ICMP Echo Request payload size') - parser_send.add_argument('--send-tc', nargs=1, type=int, + parser_send.add_argument('--send-sport', type=int, + help='TCP source port') + parser_send.add_argument('--send-dport', type=int, default=9, + help='TCP destination port') + parser_send.add_argument('--send-length', type=int, default=len(PAYLOAD_MAGIC), + help='ICMP Echo Request payload size') + parser_send.add_argument('--send-tc', type=int, help='IPv6 Traffic Class or IPv4 DiffServ / ToS') parser_send.add_argument('--send-tcpopt-unaligned', action='store_true', - help='Include unaligned TCP options') + help='Include unaligned TCP options') parser_send.add_argument('--send-nop', action='store_true', - help='Include a NOP IPv4 option') + help='Include a NOP IPv4 option') # Expectations parser_expect = parser.add_argument_group('Values expected in sniffed packets') - parser_expect.add_argument('--expect-flags', nargs=1, type=str, + parser_expect.add_argument('--expect-flags', type=str, help='IPv4 fragmentation flags') - parser_expect.add_argument('--expect-hlim', nargs=1, type=int, + parser_expect.add_argument('--expect-hlim', type=int, help='IPv6 Hop Limit or IPv4 Time To Live') - parser_expect.add_argument('--expect-mss', nargs=1, type=int, + parser_expect.add_argument('--expect-mss', type=int, help='TCP Maximum Segment Size') - parser_send.add_argument('--expect-seq', nargs=1, type=int, + parser_send.add_argument('--expect-seq', type=int, help='TCP sequence number') - parser_expect.add_argument('--expect-tc', nargs=1, type=int, + parser_expect.add_argument('--expect-tc', type=int, help='IPv6 Traffic Class or IPv4 DiffServ / ToS') parser.add_argument('-v', '--verbose', action='store_true', @@ -478,31 +668,31 @@ def main(): if args.verbose: LOGGER.setLevel(logging.DEBUG) - # Dig out real values of program arguments - send_if = args.sendif[0] - reply_ifs = args.replyif - recv_ifs = args.recvif - dst_address = args.to[0] - - # Standardize parameters which have nargs=1. + # Split parameters into send and expect parameters. Parameters might be + # missing from the command line, always fill the dictionaries with None. send_params = {} expect_params = {} - for param_name in ('flags', 'hlim', 'length', 'mss', 'seq', 'tc', 'frag_length'): + for param_name in ( + 'flags', 'hlim', 'length', 'mss', 'seq', 'tc', 'frag_length', + 'sport', 'dport', + ): param_arg = vars(args).get(f'send_{param_name}') - send_params[param_name] = param_arg[0] if param_arg else None + send_params[param_name] = param_arg if param_arg else None param_arg = vars(args).get(f'expect_{param_name}') - expect_params[param_name] = param_arg[0] if param_arg else None + expect_params[param_name] = param_arg if param_arg else None expect_params['length'] = send_params['length'] send_params['tcpopt_unaligned'] = args.send_tcpopt_unaligned send_params['nop'] = args.send_nop - send_params['src_address'] = args.fromaddr[0] if args.fromaddr else None + send_params['src_address'] = args.fromaddr if args.fromaddr else None + send_params['dst_address'] = args.to + send_params['sendif'] = args.sendif # We may not have a default route. Tell scapy where to start looking for routes - sp.conf.iface6 = send_if + sp.conf.iface6 = args.sendif # Configuration sanity checking. - if not (reply_ifs or recv_ifs): + if not (args.replyif or args.recvif): raise Exception('With no reply or recv interface specified no traffic ' 'can be sniffed and verified!' ) @@ -510,35 +700,41 @@ def main(): sniffers = [] if send_params['frag_length']: - defrag = True + if ( + (send_params['src_address'] and ':' in send_params['src_address']) or + (send_params['dst_address'] and ':' in send_params['dst_address']) + ): + defrag = 'IPv6' + else: + defrag = 'IPv4' else: defrag = False - if recv_ifs: + if args.recvif: sniffer_params = copy(expect_params) sniffer_params['src_address'] = None - sniffer_params['dst_address'] = dst_address - for iface in recv_ifs: + sniffer_params['dst_address'] = args.to + for iface in args.recvif: LOGGER.debug(f'Installing receive sniffer on {iface}') sniffers.append( setup_sniffer(iface, args.ping_type, 'request', - sniffer_params, defrag, + sniffer_params, defrag, send_params, )) - if reply_ifs: + if args.replyif: sniffer_params = copy(expect_params) - sniffer_params['src_address'] = dst_address + sniffer_params['src_address'] = args.to sniffer_params['dst_address'] = None - for iface in reply_ifs: + for iface in args.replyif: LOGGER.debug(f'Installing reply sniffer on {iface}') sniffers.append( setup_sniffer(iface, args.ping_type, 'reply', - sniffer_params, defrag, + sniffer_params, defrag, send_params, )) LOGGER.debug(f'Installed {len(sniffers)} sniffers') - send_ping(dst_address, send_if, args.ping_type, send_params) + send_ping(args.ping_type, send_params) err = 0 sniffer_num = 0 diff --git a/tests/sys/netpfil/common/rdr.sh b/tests/sys/netpfil/common/rdr.sh index 7d6297870e6c..0d6f27694c8c 100644 --- a/tests/sys/netpfil/common/rdr.sh +++ b/tests/sys/netpfil/common/rdr.sh @@ -83,6 +83,7 @@ local_redirect_body() firewall=$1 firewall_init $firewall nat_init $firewall + vnet_init_bridge bridge=$(vnet_mkbridge) ifconfig ${bridge} 192.0.2.1/24 up diff --git a/tests/sys/netpfil/common/sniffer.py b/tests/sys/netpfil/common/sniffer.py index 14305a37278c..583b27d34ca6 100644 --- a/tests/sys/netpfil/common/sniffer.py +++ b/tests/sys/netpfil/common/sniffer.py @@ -56,14 +56,19 @@ class Sniffer(threading.Thread): def run(self): self.packets = [] - if self._defrag: - # With fragment reassembly we can't stop the sniffer after catching - # the good packets, as those have not been reassembled. We must - # wait for sniffer to finish and check returned packets instead. + # With fragment reassembly we can't stop the sniffer after catching + # the good packets, as those have not been reassembled. We must + # wait for sniffer to finish and check returned packets instead. + if self._defrag == 'IPv4': self.packets = sp.sniff(session=sp.IPSession, iface=self._recvif, timeout=self._timeout, started_callback=self._startedCb) for p in self.packets: self._checkPacket(p) + elif self._defrag == 'IPv6': + self.packets = sp.sniff(session=sp.DefaultSession, iface=self._recvif, + timeout=self._timeout, started_callback=self._startedCb) + for p in sp.defragment6(self.packets): + self._checkPacket(p) else: self.packets = sp.sniff(iface=self._recvif, stop_filter=self._checkPacket, timeout=self._timeout, diff --git a/tests/sys/netpfil/common/tos.sh b/tests/sys/netpfil/common/tos.sh index 39f756be8fe9..3b689d7f67d0 100644 --- a/tests/sys/netpfil/common/tos.sh +++ b/tests/sys/netpfil/common/tos.sh @@ -33,7 +33,7 @@ tos_head() { atf_set descr 'set-tos test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } tos_body() diff --git a/tests/sys/netpfil/ipfw/Makefile b/tests/sys/netpfil/ipfw/Makefile index 1d4629c0e738..d4dbdb00f251 100644 --- a/tests/sys/netpfil/ipfw/Makefile +++ b/tests/sys/netpfil/ipfw/Makefile @@ -2,7 +2,13 @@ PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netpfil/ipfw -ATF_TESTS_SH+= fwd +ATF_TESTS_SH+= fwd \ + divert + ${PACKAGE}FILES+= fwd_inetd.conf +# Allow tests to run in parallel in their own jails +TEST_METADATA+= execenv="jail" +TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets" + .include <bsd.test.mk> diff --git a/tests/sys/netpfil/ipfw/divert.sh b/tests/sys/netpfil/ipfw/divert.sh new file mode 100644 index 000000000000..62db3f8fce98 --- /dev/null +++ b/tests/sys/netpfil/ipfw/divert.sh @@ -0,0 +1,281 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2023 Igor Ostapenko <pm@igoro.pro> +# +# 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. + +# +# ipfw divert action test cases +# +# -----------| |-- |----| ----| |----------- +# ( ) inbound |ipfw| ) -> |host| -> ( ) |ipfw| outbound ) +# -----------| | |-- |----| ----| | |----------- +# | | +# \|/ \|/ +# |------| |------| +# |divapp| |divapp| +# |------| |------| +# +# The basic cases: +# - inbound > diverted | divapp terminated +# - inbound > diverted > inbound | host terminated +# - inbound > diverted > outbound | network terminated +# - outbound > diverted | divapp terminated +# - outbound > diverted > outbound | network terminated +# - outbound > diverted > inbound | e.g. host terminated +# +# When a packet is diverted, forwarded, and possibly diverted again: +# - inbound > diverted > inbound > forwarded +# > outbound | network terminated +# - inbound > diverted > inbound > forwarded +# > outbound > diverted > outbound | network terminated +# +# Test case naming legend: +# in - inbound +# div - diverted +# out - outbound +# fwd - forwarded +# + +. $(atf_get_srcdir)/../common/utils.subr + +divert_init() +{ + if ! kldstat -q -m ipdivert; then + atf_skip "This test requires ipdivert" + fi +} + +atf_test_case "in_div" "cleanup" +in_div_head() +{ + atf_set descr 'Test inbound > diverted | divapp terminated' + atf_set require.user root +} +in_div_body() +{ + firewall_init "ipfw" + divert_init + + epair=$(vnet_mkepair) + vnet_mkjail div ${epair}b + ifconfig ${epair}a 192.0.2.1/24 up + jexec div ifconfig ${epair}b 192.0.2.2/24 up + jexec div ipfw add 65534 allow all from any to any + + # Sanity check + atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 + + jexec div ipfw add 100 divert 2000 icmp from any to any in icmptypes 8 + + jexec div $(atf_get_srcdir)/../common/divapp 2000 & + divapp_pid=$! + # Wait for the divapp to be ready + sleep 1 + + # divapp is expected to "eat" the packet + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 + + wait $divapp_pid +} +in_div_cleanup() +{ + firewall_cleanup "ipfw" +} + +atf_test_case "in_div_in" "cleanup" +in_div_in_head() +{ + atf_set descr 'Test inbound > diverted > inbound | host terminated' + atf_set require.user root +} +in_div_in_body() +{ + firewall_init "ipfw" + divert_init + + epair=$(vnet_mkepair) + vnet_mkjail div ${epair}b + ifconfig ${epair}a 192.0.2.1/24 up + jexec div ifconfig ${epair}b 192.0.2.2/24 up + jexec div ipfw add 65534 allow all from any to any + + # Sanity check + atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 + + jexec div ipfw add 100 divert 2000 icmp from any to any in icmptypes 8 + + jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back & + divapp_pid=$! + # Wait for the divapp to be ready + sleep 1 + + # divapp is NOT expected to "eat" the packet + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + wait $divapp_pid +} +in_div_in_cleanup() +{ + firewall_cleanup "ipfw" +} + +atf_test_case "out_div" "cleanup" +out_div_head() +{ + atf_set descr 'Test outbound > diverted | divapp terminated' + atf_set require.user root +} +out_div_body() +{ + firewall_init "ipfw" + divert_init + + epair=$(vnet_mkepair) + vnet_mkjail div ${epair}b + ifconfig ${epair}a 192.0.2.1/24 up + jexec div ifconfig ${epair}b 192.0.2.2/24 up + jexec div ipfw add 65534 allow all from any to any + + # Sanity check + atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 + + jexec div ipfw add 100 divert 2000 icmp from any to any out icmptypes 0 + + jexec div $(atf_get_srcdir)/../common/divapp 2000 & + divapp_pid=$! + # Wait for the divapp to be ready + sleep 1 + + # divapp is expected to "eat" the packet + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 + + wait $divapp_pid +} +out_div_cleanup() +{ + firewall_cleanup "ipfw" +} + +atf_test_case "out_div_out" "cleanup" +out_div_out_head() +{ + atf_set descr 'Test outbound > diverted > outbound | network terminated' + atf_set require.user root +} +out_div_out_body() +{ + firewall_init "ipfw" + divert_init + + epair=$(vnet_mkepair) + vnet_mkjail div ${epair}b + ifconfig ${epair}a 192.0.2.1/24 up + jexec div ifconfig ${epair}b 192.0.2.2/24 up + jexec div ipfw add 65534 allow all from any to any + + # Sanity check + atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 + + jexec div ipfw add 100 divert 2000 icmp from any to any out icmptypes 0 + + jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back & + divapp_pid=$! + # Wait for the divapp to be ready + sleep 1 + + # divapp is NOT expected to "eat" the packet + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + wait $divapp_pid +} +out_div_out_cleanup() +{ + firewall_cleanup "ipfw" +} + +atf_test_case "in_div_in_fwd_out_div_out" "cleanup" +in_div_in_fwd_out_div_out_head() +{ + atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated' + atf_set require.user root +} +in_div_in_fwd_out_div_out_body() +{ + firewall_init "ipfw" + divert_init + + # host <a--epair0--b> router <a--epair1--b> site + epair0=$(vnet_mkepair) + epair1=$(vnet_mkepair) + + vnet_mkjail router ${epair0}b ${epair1}a + ifconfig ${epair0}a 192.0.2.1/24 up + jexec router sysctl net.inet.ip.forwarding=1 + jexec router ifconfig ${epair0}b 192.0.2.2/24 up + jexec router ifconfig ${epair1}a 198.51.100.1/24 up + jexec router ipfw add 65534 allow all from any to any + + vnet_mkjail site ${epair1}b + jexec site ifconfig ${epair1}b 198.51.100.2/24 up + jexec site ipfw add 65534 allow all from any to any + jexec site route add default 198.51.100.1 + + route add -net 198.51.100.0/24 192.0.2.2 + + # Sanity check + atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 + + # Should be routed without diversion + atf_check -s exit:0 -o ignore ping -c3 198.51.100.2 + + jexec router ipfw add 100 divert 2001 icmp from any to any in icmptypes 8 + jexec router ipfw add 200 divert 2002 icmp from any to any out icmptypes 8 + + jexec router $(atf_get_srcdir)/../common/divapp 2001 divert-back & + indivapp_pid=$! + jexec router $(atf_get_srcdir)/../common/divapp 2002 divert-back & + outdivapp_pid=$! + # Wait for the divappS to be ready + sleep 1 + + # Both divappS are NOT expected to "eat" the packet + atf_check -s exit:0 -o ignore ping -c1 198.51.100.2 + + wait $indivapp_pid && wait $outdivapp_pid +} +in_div_in_fwd_out_div_out_cleanup() +{ + firewall_cleanup "ipfw" +} + +atf_init_test_cases() +{ + atf_add_test_case "in_div" + atf_add_test_case "in_div_in" + + atf_add_test_case "out_div" + atf_add_test_case "out_div_out" + + atf_add_test_case "in_div_in_fwd_out_div_out" +} diff --git a/tests/sys/netpfil/pf/Makefile b/tests/sys/netpfil/pf/Makefile index 867b98e5f6c2..3adaef09ddbd 100644 --- a/tests/sys/netpfil/pf/Makefile +++ b/tests/sys/netpfil/pf/Makefile @@ -1,12 +1,11 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netpfil/pf -BINDIR= ${TESTSDIR} TESTS_SUBDIRS+= ioctl ATF_TESTS_SH+= altq \ anchor \ + debug \ divert-to \ dup \ ether \ @@ -16,14 +15,21 @@ ATF_TESTS_SH+= altq \ fragmentation_no_reassembly \ get_state \ icmp \ + icmp6 \ + if_enc \ + limits \ loginterface \ killstate \ macro \ - map_e \ match \ + max_pkt_rate \ + max_pkt_size \ + max_states \ + mbuf \ modulate \ names \ nat \ + nat64 \ pass_block \ pflog \ pflow \ @@ -40,7 +46,9 @@ ATF_TESTS_SH+= altq \ sctp \ set_skip \ set_tos \ + snmp \ src_track \ + status \ syncookie \ synproxy \ table \ @@ -48,15 +56,21 @@ ATF_TESTS_SH+= altq \ tos ATF_TESTS_PYTEST+= frag6.py +ATF_TESTS_PYTEST+= header.py +ATF_TESTS_PYTEST+= icmp.py +ATF_TESTS_PYTEST+= nat64.py ATF_TESTS_PYTEST+= nat66.py +ATF_TESTS_PYTEST+= return.py ATF_TESTS_PYTEST+= sctp.py +ATF_TESTS_PYTEST+= tcp.py -# Tests reuse jail names and so cannot run in parallel. -TEST_METADATA+= is_exclusive=true - -PROGS= divapp +# Allow tests to run in parallel in their own jails +TEST_METADATA+= execenv="jail" +TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets" -${PACKAGE}FILES+= CVE-2019-5597.py \ +${PACKAGE}FILES+= \ + bsnmpd.conf \ + CVE-2019-5597.py \ CVE-2019-5598.py \ daytime_inetd.conf \ echo_inetd.conf \ @@ -64,17 +78,24 @@ ${PACKAGE}FILES+= CVE-2019-5597.py \ frag-overindex.py \ frag-overlimit.py \ frag-overreplace.py \ + frag-overhole.py \ + frag-adjhole.py \ pfsync_defer.py \ pft_ether.py \ pft_read_ipfix.py \ - utils.subr + rdr-srcport.py \ + utils.subr \ + utils.py +${PACKAGE}FILESMODE_bsnmpd.conf= 0555 ${PACKAGE}FILESMODE_CVE-2019-5597.py= 0555 ${PACKAGE}FILESMODE_CVE-2019-5598.py= 0555 ${PACKAGE}FILESMODE_fragcommon.py= 0555 ${PACKAGE}FILESMODE_frag-overindex.py= 0555 ${PACKAGE}FILESMODE_frag-overlimit.py= 0555 ${PACKAGE}FILESMODE_frag-overreplace.py= 0555 +${PACKAGE}FILESMODE_frag-overhole.py= 0555 +${PACKAGE}FILESMODE_frag-adjhole.py= 0555 ${PACKAGE}FILESMODE_pfsync_defer.py= 0555 ${PACKAGE}FILESMODE_pft_ether.py= 0555 ${PACKAGE}FILESMODE_pft_read_ipfix.py= 0555 diff --git a/tests/sys/netpfil/pf/altq.sh b/tests/sys/netpfil/pf/altq.sh index 57a9edf11366..416a55777849 100644 --- a/tests/sys/netpfil/pf/altq.sh +++ b/tests/sys/netpfil/pf/altq.sh @@ -156,9 +156,7 @@ codel_bridge_body() { altq_init is_altq_supported codel - if ! kldstat -q -m if_bridge; then - atf_skip "This test requires if_bridge" - fi + vnet_init_bridge epair=$(vnet_mkepair) ifconfig ${epair}a 192.0.2.1/24 up @@ -214,7 +212,7 @@ prioritise_body() ifconfig ${epair}a 192.0.2.1/24 up jexec altq_prioritise ifconfig ${epair}b 192.0.2.2/24 up - jexec altq_prioritise /usr/sbin/inetd -p inetd-altq.pid \ + jexec altq_prioritise /usr/sbin/inetd -p ${PWD}/inetd-altq.pid \ $(atf_get_srcdir)/../pf/echo_inetd.conf # Sanity check diff --git a/tests/sys/netpfil/pf/anchor.sh b/tests/sys/netpfil/pf/anchor.sh index b1faa5f6c57e..64ca84b34c3d 100644 --- a/tests/sys/netpfil/pf/anchor.sh +++ b/tests/sys/netpfil/pf/anchor.sh @@ -58,6 +58,37 @@ pr183198_cleanup() pft_cleanup } +atf_test_case "pr279225" "cleanup" +pr279225_head() +{ + atf_set descr "Test that we can retrieve longer anchor names, PR 279225" + atf_set require.user root +} + +pr279225_body() +{ + pft_init + + vnet_mkjail alcatraz + + pft_set_rules alcatraz \ + "nat-anchor \"appjail-nat/jail/*\" all" \ + "rdr-anchor \"appjail-rdr/*\" all" \ + "anchor \"appjail/jail/*\" all" + + atf_check -s exit:0 -o match:"nat-anchor \"appjail-nat/jail/\*\" all \{" \ + jexec alcatraz pfctl -sn -a "*" + atf_check -s exit:0 -o match:"rdr-anchor \"appjail-rdr/\*\" all \{" \ + jexec alcatraz pfctl -sn -a "*" + atf_check -s exit:0 -o match:"anchor \"appjail/jail/\*\" all \{" \ + jexec alcatraz pfctl -sr -a "*" +} + +pr279225_cleanup() +{ + pft_cleanup +} + atf_test_case "nested_anchor" "cleanup" nested_anchor_head() { @@ -164,10 +195,316 @@ nested_label_cleanup() pft_cleanup } +atf_test_case "quick" "cleanup" +quick_head() +{ + atf_set descr "Test handling of quick on anchors" + atf_set require.user root +} + +quick_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "anchor quick {\n\ + pass\n\ + }" \ + "block" + + # We can still ping because the anchor is 'quick' + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + jexec alcatraz pfctl -sr -v + jexec alcatraz pfctl -ss -v +} + +quick_cleanup() +{ + pft_cleanup +} + +atf_test_case "quick_nested" "cleanup" +quick_nested_head() +{ + atf_set descr 'Verify that a nested anchor does not clear quick' + atf_set require.user root +} + +quick_nested_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "anchor quick {\n\ + pass\n\ + anchor {\n\ + block proto tcp\n\ + }\n\ + }" \ + "block" + ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -sr -v + jexec alcatraz pfctl -ss -v + + # We can still ping because the anchor is 'quick' + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + jexec alcatraz pfctl -sr -v + jexec alcatraz pfctl -ss -v +} + +quick_nested_cleanup() +{ + pft_cleanup +} + +atf_test_case "counter" "cleanup" +counter_head() +{ + atf_set descr 'Test counters on anchors' + atf_set require.user root +} + +counter_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "anchor \"foo\" {\n\ + pass\n\ + }" + + # Generate traffic + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + atf_check -s exit:0 -e ignore \ + -o match:'[ Evaluations: 1 Packets: 2 Bytes: 168 States: 1 ]' \ + jexec alcatraz pfctl -sr -vv +} + +counter_cleanup() +{ + pft_cleanup +} + +atf_test_case "nat" "cleanup" +nat_head() +{ + atf_set descr 'Test nested nat anchors' + atf_set require.user root +} + +nat_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "nat-anchor \"foo/*\"" \ + "pass" + + echo "nat log on ${epair}a inet from 192.0.2.0/24 to any port = 53 -> 192.0.2.1" \ + | jexec alcatraz pfctl -a "foo/bar" -g -f - + echo "rdr on ${epair}a proto tcp to port echo -> 127.0.0.1 port echo" \ + | jexec alcatraz pfctl -a "foo/baz" -g -f - + + jexec alcatraz pfctl -sn -a "*" + jexec alcatraz pfctl -sn -a "foo/bar" + jexec alcatraz pfctl -sn -a "foo/baz" + + atf_check -s exit:0 -o match:"nat log on ${epair}a inet from 192.0.2.0/24 to any port = domain -> 192.0.2.1" \ + jexec alcatraz pfctl -sn -a "*" + atf_check -s exit:0 -o match:"rdr on ${epair}a inet proto tcp from any to any port = echo -> 127.0.0.1 port 7" \ + jexec alcatraz pfctl -sn -a "*" +} + +nat_cleanup() +{ + pft_cleanup +} + +atf_test_case "include" "cleanup" +include_head() +{ + atf_set descr 'Test including inside anchors' + atf_set require.user root +} + +include_body() +{ + pft_init + + wd=`pwd` + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + echo "pass" > ${wd}/extra.conf + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" \ + "anchor \"foo\" {\n\ + include \"${wd}/extra.conf\"\n\ + }" + + jexec alcatraz pfctl -sr + + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 +} + +include_cleanup() +{ + pft_cleanup +} + +atf_test_case "quick" "cleanup" +quick_head() +{ + atf_set descr 'Test quick on anchors' + atf_set require.user root +} + +quick_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "anchor quick {\n\ + pass\n\ + }" \ + "block" + + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + jexec alcatraz pfctl -sr -vv -a "*" +} + +quick_cleanup() +{ + pft_cleanup +} + +atf_test_case "recursive_flush" "cleanup" +recursive_flush_head() +{ + atf_set descr 'Test recursive flushing of rules' + atf_set require.user root +} + +recursive_flush_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" \ + "anchor \"foo\" {\n\ + pass\n\ + }" + + # We can ping thanks to the pass rule in foo + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + # Only reset the main rules. I.e. not a recursive flush + pft_set_rules alcatraz \ + "block" \ + "anchor \"foo\"" + + # "foo" still has the pass rule, so this works + jexec alcatraz pfctl -a "*" -sr + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + # Now do a recursive flush + atf_check -s exit:0 -e ignore -o ignore \ + jexec alcatraz pfctl -a "*" -Fr + pft_set_rules alcatraz \ + "block" \ + "anchor \"foo\"" + + # So this fails + jexec alcatraz pfctl -a "*" -sr + atf_check -s exit:2 -o ignore ping -c 1 192.0.2.1 +} + +recursive_flush_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "pr183198" + atf_add_test_case "pr279225" atf_add_test_case "nested_anchor" atf_add_test_case "wildcard" atf_add_test_case "nested_label" + atf_add_test_case "quick" + atf_add_test_case "quick_nested" + atf_add_test_case "counter" + atf_add_test_case "nat" + atf_add_test_case "include" + atf_add_test_case "quick" + atf_add_test_case "recursive_flush" } diff --git a/tests/sys/netpfil/pf/bsnmpd.conf b/tests/sys/netpfil/pf/bsnmpd.conf new file mode 100644 index 000000000000..27abdda6cbd3 --- /dev/null +++ b/tests/sys/netpfil/pf/bsnmpd.conf @@ -0,0 +1,47 @@ +location := "A galaxy far, far away" +contact := "skywalker@Tatooine" +system := 1 + +read := "public" +write := "geheim" +trap := "mytrap" + +NoAuthProtocol := 1.3.6.1.6.3.10.1.1.1 +HMACMD5AuthProtocol := 1.3.6.1.6.3.10.1.1.2 +HMACSHAAuthProtocol := 1.3.6.1.6.3.10.1.1.3 +NoPrivProtocol := 1.3.6.1.6.3.10.1.2.1 +DESPrivProtocol := 1.3.6.1.6.3.10.1.2.2 +AesCfb128Protocol := 1.3.6.1.6.3.10.1.2.4 + +securityModelAny := 0 +securityModelSNMPv1 := 1 +securityModelSNMPv2c := 2 +securityModelUSM := 3 + +MPmodelSNMPv1 := 0 +MPmodelSNMPv2c := 1 +MPmodelSNMPv3 := 3 + +noAuthNoPriv := 1 +authNoPriv := 2 +authPriv := 3 + +%snmpd +begemotSnmpdDebugDumpPdus = 2 +begemotSnmpdDebugSyslogPri = 7 + +begemotSnmpdCommunityString.0.1 = $(read) +begemotSnmpdCommunityDisable = 1 + +begemotSnmpdTransInetStatus.1.4.0.0.0.0.161.1 = 4 +begemotSnmpdTransInetStatus.2.16.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.161.1 = 4 + +begemotSnmpdLocalPortStatus."/var/run/snmpd.sock" = 1 +begemotSnmpdLocalPortType."/var/run/snmpd.sock" = 4 + +sysContact = $(contact) +sysLocation = $(location) +sysObjectId = 1.3.6.1.4.1.12325.1.1.2.1.$(system) + +begemotSnmpdModulePath."mibII" = "/usr/lib/snmp_mibII.so" +begemotSnmpdModulePath."pf" = "/usr/lib/snmp_pf.so" diff --git a/tests/sys/netpfil/pf/debug.sh b/tests/sys/netpfil/pf/debug.sh new file mode 100644 index 000000000000..404d37ab8932 --- /dev/null +++ b/tests/sys/netpfil/pf/debug.sh @@ -0,0 +1,106 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Rubicon Communications, LLC (Netgate) +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Test setting and retrieving debug level' + atf_set require.user root +} + +basic_body() +{ + pft_init + + vnet_mkjail debug + atf_check -s exit:0 -e ignore \ + jexec debug pfctl -x loud + + atf_check -s exit:0 -o match:'Debug: Loud' \ + jexec debug pfctl -si +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_test_case "reset" "cleanup" +reset_head() +{ + atf_set descr 'Test resetting debug level' + atf_set require.user root +} + +reset_body() +{ + pft_init + + vnet_mkjail debug + + # Default is Urgent + atf_check -s exit:0 -o match:'Debug: Urgent' \ + jexec debug pfctl -sa + state_limit=$(jexec debug pfctl -sa | grep 'states.*hard limit' | awk '{ print $4; }') + + # Change defaults + pft_set_rules debug \ + "set limit states 42" + atf_check -s exit:0 -e ignore \ + jexec debug pfctl -x loud + + atf_check -s exit:0 -o match:'Debug: Loud' \ + jexec debug pfctl -sa + new_state_limit=$(jexec debug pfctl -sa | grep 'states.*hard limit' | awk '{ print $4; }') + if [ $state_limit -eq $new_state_limit ]; then + jexec debug pfctl -sa + atf_fail "Failed to change state limit" + fi + + # Reset + atf_check -s exit:0 -o ignore -e ignore \ + jexec debug pfctl -FR + atf_check -s exit:0 -o match:'Debug: Urgent' \ + jexec debug pfctl -sa + new_state_limit=$(jexec debug pfctl -sa | grep 'states.*hard limit' | awk '{ print $4; }') + if [ $state_limit -ne $new_state_limit ]; then + jexec debug pfctl -sa + atf_fail "Failed to reset state limit" + fi +} + +reset_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" + atf_add_test_case "reset" +} diff --git a/tests/sys/netpfil/pf/divert-to.sh b/tests/sys/netpfil/pf/divert-to.sh index 72adbeedb007..ae44cd5d51af 100644 --- a/tests/sys/netpfil/pf/divert-to.sh +++ b/tests/sys/netpfil/pf/divert-to.sh @@ -51,8 +51,6 @@ # > outbound > diverted > outbound | network terminated # # Test case naming legend: -# ipfwon - with ipfw enabled -# ipfwoff - with ipfw disabled # in - inbound # div - diverted # out - outbound @@ -76,40 +74,21 @@ dummynet_init() fi } -ipfw_init() -{ - if ! kldstat -q -m ipfw; then - atf_skip "This test requires ipfw" - fi -} - -assert_ipfw_is_off() -{ - if kldstat -q -m ipfw; then - atf_skip "This test is for the case when ipfw is not loaded" - fi -} - -atf_test_case "ipfwoff_in_div" "cleanup" -ipfwoff_in_div_head() +atf_test_case "in_div" "cleanup" +in_div_head() { atf_set descr 'Test inbound > diverted | divapp terminated' atf_set require.user root } -ipfwoff_in_div_body() +in_div_body() { - local ipfwon - pft_init divert_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off epair=$(vnet_mkepair) vnet_mkjail div ${epair}b ifconfig ${epair}a 192.0.2.1/24 up jexec div ifconfig ${epair}b 192.0.2.2/24 up - test $ipfwon && jexec div ipfw add 65534 allow all from any to any # Sanity check atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 @@ -119,56 +98,36 @@ ipfwoff_in_div_body() "pass all" \ "pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2000" - jexec div $(atf_get_srcdir)/divapp 2000 & + jexec div $(atf_get_srcdir)/../common/divapp 2000 & divapp_pid=$! # Wait for the divapp to be ready sleep 1 # divapp is expected to "eat" the packet - atf_check -s not-exit:0 -o ignore ping -c1 192.0.2.2 + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 wait $divapp_pid } -ipfwoff_in_div_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwon_in_div" "cleanup" -ipfwon_in_div_head() -{ - atf_set descr 'Test inbound > diverted | divapp terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_in_div_body() -{ - ipfwoff_in_div_body "ipfwon" -} -ipfwon_in_div_cleanup() +in_div_cleanup() { pft_cleanup } -atf_test_case "ipfwoff_in_div_in" "cleanup" -ipfwoff_in_div_in_head() +atf_test_case "in_div_in" "cleanup" +in_div_in_head() { atf_set descr 'Test inbound > diverted > inbound | host terminated' atf_set require.user root } -ipfwoff_in_div_in_body() +in_div_in_body() { - local ipfwon - pft_init divert_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off epair=$(vnet_mkepair) vnet_mkjail div ${epair}b ifconfig ${epair}a 192.0.2.1/24 up jexec div ifconfig ${epair}b 192.0.2.2/24 up - test $ipfwon && jexec div ipfw add 65534 allow all from any to any # Sanity check atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 @@ -178,7 +137,7 @@ ipfwoff_in_div_in_body() "pass all" \ "pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2000 no state" - jexec div $(atf_get_srcdir)/divapp 2000 divert-back & + jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back & divapp_pid=$! # Wait for the divapp to be ready sleep 1 @@ -188,46 +147,26 @@ ipfwoff_in_div_in_body() wait $divapp_pid } -ipfwoff_in_div_in_cleanup() +in_div_in_cleanup() { pft_cleanup } -atf_test_case "ipfwon_in_div_in" "cleanup" -ipfwon_in_div_in_head() -{ - atf_set descr 'Test inbound > diverted > inbound | host terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_in_div_in_body() -{ - ipfwoff_in_div_in_body "ipfwon" -} -ipfwon_in_div_in_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwoff_out_div" "cleanup" -ipfwoff_out_div_head() +atf_test_case "out_div" "cleanup" +out_div_head() { atf_set descr 'Test outbound > diverted | divapp terminated' atf_set require.user root } -ipfwoff_out_div_body() +out_div_body() { - local ipfwon - pft_init divert_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off epair=$(vnet_mkepair) vnet_mkjail div ${epair}b ifconfig ${epair}a 192.0.2.1/24 up jexec div ifconfig ${epair}b 192.0.2.2/24 up - test $ipfwon && jexec div ipfw add 65534 allow all from any to any # Sanity check atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 @@ -238,56 +177,36 @@ ipfwoff_out_div_body() "pass in inet proto icmp icmp-type echoreq no state" \ "pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2000 no state" - jexec div $(atf_get_srcdir)/divapp 2000 & + jexec div $(atf_get_srcdir)/../common/divapp 2000 & divapp_pid=$! # Wait for the divapp to be ready sleep 1 # divapp is expected to "eat" the packet - atf_check -s not-exit:0 -o ignore ping -c1 192.0.2.2 + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 wait $divapp_pid } -ipfwoff_out_div_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwon_out_div" "cleanup" -ipfwon_out_div_head() -{ - atf_set descr 'Test outbound > diverted | divapp terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_out_div_body() -{ - ipfwoff_out_div_body "ipfwon" -} -ipfwon_out_div_cleanup() +out_div_cleanup() { pft_cleanup } -atf_test_case "ipfwoff_out_div_out" "cleanup" -ipfwoff_out_div_out_head() +atf_test_case "out_div_out" "cleanup" +out_div_out_head() { atf_set descr 'Test outbound > diverted > outbound | network terminated' atf_set require.user root } -ipfwoff_out_div_out_body() +out_div_out_body() { - local ipfwon - pft_init divert_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off epair=$(vnet_mkepair) vnet_mkjail div ${epair}b ifconfig ${epair}a 192.0.2.1/24 up jexec div ifconfig ${epair}b 192.0.2.2/24 up - test $ipfwon && jexec div ipfw add 65534 allow all from any to any # Sanity check atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 @@ -298,7 +217,7 @@ ipfwoff_out_div_out_body() "pass in inet proto icmp icmp-type echoreq no state" \ "pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2000 no state" - jexec div $(atf_get_srcdir)/divapp 2000 divert-back & + jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back & divapp_pid=$! # Wait for the divapp to be ready sleep 1 @@ -308,40 +227,21 @@ ipfwoff_out_div_out_body() wait $divapp_pid } -ipfwoff_out_div_out_cleanup() +out_div_out_cleanup() { pft_cleanup } -atf_test_case "ipfwon_out_div_out" "cleanup" -ipfwon_out_div_out_head() -{ - atf_set descr 'Test outbound > diverted > outbound | network terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_out_div_out_body() -{ - ipfwoff_out_div_out_body "ipfwon" -} -ipfwon_out_div_out_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwoff_in_div_in_fwd_out_div_out" "cleanup" -ipfwoff_in_div_in_fwd_out_div_out_head() +atf_test_case "in_div_in_fwd_out_div_out" "cleanup" +in_div_in_fwd_out_div_out_head() { atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated' atf_set require.user root } -ipfwoff_in_div_in_fwd_out_div_out_body() +in_div_in_fwd_out_div_out_body() { - local ipfwon - pft_init divert_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off # host <a--epair0--b> router <a--epair1--b> site epair0=$(vnet_mkepair) @@ -352,12 +252,10 @@ ipfwoff_in_div_in_fwd_out_div_out_body() jexec router sysctl net.inet.ip.forwarding=1 jexec router ifconfig ${epair0}b 192.0.2.2/24 up jexec router ifconfig ${epair1}a 198.51.100.1/24 up - test $ipfwon && jexec router ipfw add 65534 allow all from any to any vnet_mkjail site ${epair1}b jexec site ifconfig ${epair1}b 198.51.100.2/24 up jexec site route add default 198.51.100.1 - test $ipfwon && jexec site ipfw add 65534 allow all from any to any route add -net 198.51.100.0/24 192.0.2.2 @@ -373,9 +271,9 @@ ipfwoff_in_div_in_fwd_out_div_out_body() "pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2001 no state" \ "pass out inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2002 no state" - jexec router $(atf_get_srcdir)/divapp 2001 divert-back & + jexec router $(atf_get_srcdir)/../common/divapp 2001 divert-back & indivapp_pid=$! - jexec router $(atf_get_srcdir)/divapp 2002 divert-back & + jexec router $(atf_get_srcdir)/../common/divapp 2002 divert-back & outdivapp_pid=$! # Wait for the divappS to be ready sleep 1 @@ -385,48 +283,28 @@ ipfwoff_in_div_in_fwd_out_div_out_body() wait $indivapp_pid && wait $outdivapp_pid } -ipfwoff_in_div_in_fwd_out_div_out_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwon_in_div_in_fwd_out_div_out" "cleanup" -ipfwon_in_div_in_fwd_out_div_out_head() -{ - atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_in_div_in_fwd_out_div_out_body() -{ - ipfwoff_in_div_in_fwd_out_div_out_body "ipfwon" -} -ipfwon_in_div_in_fwd_out_div_out_cleanup() +in_div_in_fwd_out_div_out_cleanup() { pft_cleanup } -atf_test_case "ipfwoff_in_dn_in_div_in_out_div_out_dn_out" "cleanup" -ipfwoff_in_dn_in_div_in_out_div_out_dn_out_head() +atf_test_case "in_dn_in_div_in_out_div_out_dn_out" "cleanup" +in_dn_in_div_in_out_div_out_dn_out_head() { atf_set descr 'Test inbound > delayed+diverted > outbound > diverted+delayed > outbound | network terminated' atf_set require.user root } -ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body() +in_dn_in_div_in_out_div_out_dn_out_body() { - local ipfwon - pft_init divert_init dummynet_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off epair=$(vnet_mkepair) vnet_mkjail alcatraz ${epair}b ifconfig ${epair}a 192.0.2.1/24 up ifconfig ${epair}a ether 02:00:00:00:00:01 jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up - test $ipfwon && jexec alcatraz ipfw add 65534 allow all from any to any # Sanity check atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 @@ -444,9 +322,9 @@ ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body() "pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 1001 no state" \ "pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 1002 no state" - jexec alcatraz $(atf_get_srcdir)/divapp 1001 divert-back & + jexec alcatraz $(atf_get_srcdir)/../common/divapp 1001 divert-back & indivapp_pid=$! - jexec alcatraz $(atf_get_srcdir)/divapp 1002 divert-back & + jexec alcatraz $(atf_get_srcdir)/../common/divapp 1002 divert-back & outdivapp_pid=$! # Wait for the divappS to be ready sleep 1 @@ -473,9 +351,9 @@ ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body() "pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2001 no state" \ "pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2002 no state" - jexec alcatraz $(atf_get_srcdir)/divapp 2001 divert-back & + jexec alcatraz $(atf_get_srcdir)/../common/divapp 2001 divert-back & indivapp_pid=$! - jexec alcatraz $(atf_get_srcdir)/divapp 2002 divert-back & + jexec alcatraz $(atf_get_srcdir)/../common/divapp 2002 divert-back & outdivapp_pid=$! # Wait for the divappS to be ready sleep 1 @@ -489,41 +367,20 @@ ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body() # } } -ipfwoff_in_dn_in_div_in_out_div_out_dn_out_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwon_in_dn_in_div_in_out_div_out_dn_out" "cleanup" -ipfwon_in_dn_in_div_in_out_div_out_dn_out_head() -{ - atf_set descr 'Test inbound > delayed+diverted > outbound > diverted+delayed > outbound | network terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_in_dn_in_div_in_out_div_out_dn_out_body() -{ - ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body "ipfwon" -} -ipfwon_in_dn_in_div_in_out_div_out_dn_out_cleanup() +in_dn_in_div_in_out_div_out_dn_out_cleanup() { pft_cleanup } atf_init_test_cases() { - atf_add_test_case "ipfwoff_in_div" - atf_add_test_case "ipfwoff_in_div_in" - atf_add_test_case "ipfwon_in_div" - atf_add_test_case "ipfwon_in_div_in" + atf_add_test_case "in_div" + atf_add_test_case "in_div_in" - atf_add_test_case "ipfwoff_out_div" - atf_add_test_case "ipfwoff_out_div_out" - atf_add_test_case "ipfwon_out_div" - atf_add_test_case "ipfwon_out_div_out" + atf_add_test_case "out_div" + atf_add_test_case "out_div_out" - atf_add_test_case "ipfwoff_in_div_in_fwd_out_div_out" - atf_add_test_case "ipfwon_in_div_in_fwd_out_div_out" + atf_add_test_case "in_div_in_fwd_out_div_out" - atf_add_test_case "ipfwoff_in_dn_in_div_in_out_div_out_dn_out" - atf_add_test_case "ipfwon_in_dn_in_div_in_out_div_out_dn_out" + atf_add_test_case "in_dn_in_div_in_out_div_out_dn_out" } diff --git a/tests/sys/netpfil/pf/dup.sh b/tests/sys/netpfil/pf/dup.sh index 68631251144a..64a08083bca0 100644 --- a/tests/sys/netpfil/pf/dup.sh +++ b/tests/sys/netpfil/pf/dup.sh @@ -33,7 +33,7 @@ dup_to_head() { atf_set descr 'dup-to test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } dup_to_body() diff --git a/tests/sys/netpfil/pf/ether.sh b/tests/sys/netpfil/pf/ether.sh index 9a1ab1b005d7..f0fdce50a7d3 100644 --- a/tests/sys/netpfil/pf/ether.sh +++ b/tests/sys/netpfil/pf/ether.sh @@ -286,7 +286,7 @@ captive_body() # Run the echo server only on the gw, so we know we've redirectly # correctly if we get an echo message. - jexec gw /usr/sbin/inetd $(atf_get_srcdir)/echo_inetd.conf + jexec gw /usr/sbin/inetd -p ${PWD}/echo_inetd.pid $(atf_get_srcdir)/echo_inetd.conf # Confirm that we're getting redirected atf_check -s exit:0 -o match:"^foo$" -x "echo foo | nc -N 198.51.100.2 7" @@ -304,7 +304,7 @@ captive_body() atf_check -s exit:1 -x "echo foo | nc -N 198.51.100.2 7" # Start a server in srv - jexec srv /usr/sbin/inetd $(atf_get_srcdir)/echo_inetd.conf + jexec srv /usr/sbin/inetd -p ${PWD}/echo_inetd.pid $(atf_get_srcdir)/echo_inetd.conf # And now we can talk to that one. atf_check -s exit:0 -o match:"^foo$" -x "echo foo | nc -N 198.51.100.2 7" @@ -362,8 +362,8 @@ captive_long_body() # ICMP should still work, because we don't redirect it. atf_check -s exit:0 -o ignore ping -c 1 -t 1 198.51.100.2 - jexec gw /usr/sbin/inetd -p gw.pid $(atf_get_srcdir)/echo_inetd.conf - jexec srv /usr/sbin/inetd -p srv.pid $(atf_get_srcdir)/daytime_inetd.conf + jexec gw /usr/sbin/inetd -p ${PWD}/gw.pid $(atf_get_srcdir)/echo_inetd.conf + jexec srv /usr/sbin/inetd -p ${PWD}/srv.pid $(atf_get_srcdir)/daytime_inetd.conf echo foo | nc -N 198.51.100.2 13 @@ -415,7 +415,7 @@ dummynet_body() # Sanity check atf_check -s exit:0 -o ignore ping -i .1 -c 3 -s 1200 192.0.2.2 - jexec alcatraz dnctl pipe 1 config bw 30Byte/s + jexec alcatraz dnctl pipe 1 config bw 300Byte/s jexec alcatraz pfctl -e pft_set_rules alcatraz \ "ether pass in dnpipe 1" @@ -430,14 +430,14 @@ dummynet_body() ping -i .1 -c 5 -s 1200 192.0.2.2 # We should now be hitting the limits and get this packet dropped. - atf_check -s exit:2 -o ignore ping -c 1 -s 1200 192.0.2.2 + atf_check -s exit:2 -o ignore ping -c 1 -t 1 -s 1200 192.0.2.2 # We can now also dummynet outbound traffic! pft_set_rules alcatraz \ "ether pass out dnpipe 1" # We should still be hitting the limits and get this packet dropped. - atf_check -s exit:2 -o ignore ping -c 1 -s 1200 192.0.2.2 + atf_check -s exit:2 -o ignore ping -c 1 -t 1 -s 1200 192.0.2.2 } dummynet_cleanup() @@ -643,7 +643,7 @@ short_pkt_head() { atf_set descr 'Test overly short Ethernet packets' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } short_pkt_body() @@ -686,7 +686,7 @@ bridge_to_head() { atf_set descr 'Test bridge-to keyword' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } bridge_to_body() diff --git a/tests/sys/netpfil/pf/forward.sh b/tests/sys/netpfil/pf/forward.sh index 31abfad82c13..5d7d48a5dd9a 100644 --- a/tests/sys/netpfil/pf/forward.sh +++ b/tests/sys/netpfil/pf/forward.sh @@ -35,7 +35,7 @@ v4_head() atf_set require.user root # We need scapy to be installed for out test scripts to work - atf_set require.progs scapy + atf_set require.progs python3 scapy } v4_body() @@ -94,7 +94,7 @@ v6_head() { atf_set descr 'Basic IPv6 forwarding test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() diff --git a/tests/sys/netpfil/pf/frag-adjhole.py b/tests/sys/netpfil/pf/frag-adjhole.py new file mode 100644 index 000000000000..99caf66617dd --- /dev/null +++ b/tests/sys/netpfil/pf/frag-adjhole.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 Alexander Bluhm <bluhm@openbsd.org> + +from fragcommon import * + +# |--------| +# |--------| +# |-------| +# |----| + +def send(src, dst, send_if, recv_if): + pid = os.getpid() + eid = pid & 0xffff + payload = b"ABCDEFGHIJKLMNOP" * 2 + packet = sp.IP(src=src, dst=dst)/ \ + sp.ICMP(type='echo-request', id=eid) / payload + frag = [] + fid = pid & 0xffff + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + flags='MF') / bytes(packet)[20:36]) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=2, flags='MF') / bytes(packet)[36:52]) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=1, flags='MF') / bytes(packet)[28:44]) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=4) / bytes(packet)[52:60]) + eth=[] + for f in frag: + eth.append(sp.Ether()/f) + if os.fork() == 0: + time.sleep(1) + sp.sendp(eth, iface=send_if) + os._exit(0) + + ans = sp.sniff(iface=recv_if, timeout=3, filter= + "ip and src " + dst + " and dst " + src + " and icmp") + for a in ans: + if a and a.type == sp.ETH_P_IP and \ + a.payload.proto == 1 and \ + a.payload.frag == 0 and a.payload.flags == 0 and \ + sp.icmptypes[a.payload.payload.type] == 'echo-reply': + id = a.payload.payload.id + print("id=%#x" % (id)) + if id != eid: + print("WRONG ECHO REPLY ID") + exit(2) + data = a.payload.payload.payload.load + print("payload=%s" % (data)) + if data == payload: + exit(0) + print("PAYLOAD!=%s" % (payload)) + exit(1) + print("NO ECHO REPLY") + exit(2) + +if __name__ == '__main__': + main(send) diff --git a/tests/sys/netpfil/pf/frag-overhole.py b/tests/sys/netpfil/pf/frag-overhole.py new file mode 100644 index 000000000000..91697b6b83c6 --- /dev/null +++ b/tests/sys/netpfil/pf/frag-overhole.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 Alexander Bluhm <bluhm@openbsd.org> + +from fragcommon import * + +# index boundary 4096 | +# |--------------| +# .... +# |--------------| +# |----------| +# |XXXX----------| +# |XXXX----| +# |---| + +# this should trigger "frag tail overlap %d" and "frag head overlap %d" +def send(src, dst, send_if, recv_if): + pid = os.getpid() + eid = pid & 0xffff + payload = b"ABCDEFGHIJKLMNOP" + dummy = b"01234567" + fragsize = 1024 + boundary = 4096 + fragnum = int(boundary / fragsize) + packet = sp.IP(src=src, dst=dst)/ \ + sp.ICMP(type='echo-request', id=eid)/ \ + ((int((boundary + fragsize) / len(payload)) + 1) * payload) + packet_length = len(packet) + frag = [] + fid = pid & 0xffff + for i in range(fragnum-1): + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=(i * fragsize)>>3, flags='MF')/ + bytes(packet)[20 + i * fragsize:20 + (i + 1) * fragsize]) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=(boundary - fragsize) >> 3, flags='MF')/ + bytes(packet)[20 + boundary - fragsize:20 + boundary - len(dummy)]) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=(boundary - len(dummy)) >> 3, flags='MF')/ + (dummy+bytes(packet)[20 + boundary:20 + boundary + fragsize])) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=(boundary - 8 - len(dummy)) >> 3, flags='MF')/ + (dummy+bytes(packet)[20 + boundary - 8:20 + boundary])) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=(boundary + fragsize) >> 3)/bytes(packet)[20 + boundary + fragsize:]) + eth=[] + for f in frag: + eth.append(sp.Ether() / f) + + if os.fork() == 0: + time.sleep(1) + for e in eth: + sp.sendp(e, iface=send_if) + time.sleep(0.001) + os._exit(0) + + ans = sp.sniff(iface=recv_if, timeout=3, filter= + "ip and src " + dst + " and dst " + src + " and icmp") + for a in ans: + if a and a.type == sp.ETH_P_IP and \ + a.payload.proto == 1 and \ + a.payload.frag == 0 and \ + sp.icmptypes[a.payload.payload.type] == 'echo-reply': + id = a.payload.payload.id + print("id=%#x" % (id)) + if id != eid: + print("WRONG ECHO REPLY ID") + exit(2) + if a and a.type == sp.ETH_P_IP and \ + a.payload.proto == 1 and \ + a.payload.frag > 0 and \ + a.payload.flags == '': + length = (a.payload.frag << 3) + a.payload.len + print("len=%d" % (length)) + if length != packet_length: + print("WRONG ECHO REPLY LENGTH") + exit(1) + exit(0) + print("NO ECHO REPLY") + exit(1) + +if __name__ == '__main__': + main(send) diff --git a/tests/sys/netpfil/pf/frag6.py b/tests/sys/netpfil/pf/frag6.py index 28b1829d418c..26ae7af7c90c 100644 --- a/tests/sys/netpfil/pf/frag6.py +++ b/tests/sys/netpfil/pf/frag6.py @@ -1,25 +1,13 @@ import pytest import logging -import threading -import time +import random logging.getLogger("scapy").setLevel(logging.CRITICAL) +from utils import DelayedSend from atf_python.sys.net.tools import ToolsHelper from atf_python.sys.net.vnet import VnetTestTemplate -class DelayedSend(threading.Thread): - def __init__(self, packet): - threading.Thread.__init__(self) - self._packet = packet - - self.start() - - def run(self): - import scapy.all as sp - time.sleep(1) - sp.send(self._packet) - class TestFrag6(VnetTestTemplate): - REQUIRED_MODULES = ["pf"] + REQUIRED_MODULES = ["pf", "dummymbuf"] TOPOLOGY = { "vnet1": {"ifaces": ["if1"]}, "vnet2": {"ifaces": ["if1"]}, @@ -27,18 +15,22 @@ class TestFrag6(VnetTestTemplate): } def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ - "scrub fragment reassemble", + "scrub fragment reassemble min-ttl 10", "pass", "block in inet6 proto icmp6 icmp6-type echoreq", ]) + ToolsHelper.print_output("/sbin/pfilctl link -i dummymbuf:inet6 inet6") + ToolsHelper.print_output("/sbin/sysctl net.dummymbuf.rules=\"inet6 in %s enlarge 3000;\"" % ifname) def check_ping_reply(self, packet): print(packet) return False @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) def test_dup_frag_hdr(self): "Test packets with duplicate fragment headers" srv_vnet = self.vnet_map["vnet2"] @@ -58,3 +50,227 @@ class TestFrag6(VnetTestTemplate): timeout=3) for p in packets: assert not p.getlayer(sp.ICMPv6EchoReply) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_overlong(self): + "Test overly long fragmented packet" + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + curr = 0 + pkts = [] + + frag_id = random.randint(0,0xffffffff) + gran = 1200 + + i = 0 + while curr <= 65535: + ipv61 = sp.IPv6(src="2001:db8::1", dst="2001:db8::2") + more = True + g = gran + if curr + gran > 65535: + more = False + g = 65530 - curr + if i == 0: + pkt = ipv61 / sp.IPv6ExtHdrHopByHop(options=[sp.PadN(optlen=2), sp.Pad1()]) / \ + sp.IPv6ExtHdrFragment(id = frag_id, offset = curr // 8, m = more) / bytes([i] * g) + else: + pkt = ipv61 / sp.IPv6ExtHdrFragment(id = frag_id, offset = curr // 8, m = more) / bytes([i] * g) + pkts.append(pkt) + curr += gran + i += 1 + + sp.send(pkts, inter = 0.1) + +class TestFrag6HopHyHop(VnetTestTemplate): + REQUIRED_MODULES = ["pf"] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1", "if2"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "if1": {"prefixes6": [("2001:db8::1/64", "2001:db8::2/64")]}, + "if2": {"prefixes6": [("2001:db8:666::1/64", "2001:db8:1::2/64")]}, + } + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1") + ToolsHelper.print_output("/usr/sbin/ndp -s 2001:db8:1::1 00:01:02:03:04:05") + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.pf_rules([ + "scrub fragment reassemble min-ttl 10", + "pass allow-opts", + ]) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_hop_by_hop(self): + "Verify that we reject non-first hop-by-hop headers" + if1 = self.vnet.iface_alias_map["if1"].name + if2 = self.vnet.iface_alias_map["if2"].name + ToolsHelper.print_output("/sbin/route add -6 default 2001:db8::2") + ToolsHelper.print_output("/sbin/ping6 -c 1 2001:db8:1::2") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # A hop-by-hop header is accepted if it's the first header + pkt = sp.IPv6(src="2001:db8::1", dst="2001:db8:1::1") \ + / sp.IPv6ExtHdrHopByHop() \ + / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 30)) + pkt.show() + + # Delay the send so the sniffer is running when we transmit. + s = DelayedSend(pkt) + + replies = sp.sniff(iface=if2, timeout=3) + found = False + for p in replies: + p.show() + ip6 = p.getlayer(sp.IPv6) + hbh = p.getlayer(sp.IPv6ExtHdrHopByHop) + icmp6 = p.getlayer(sp.ICMPv6EchoRequest) + + if not ip6 or not icmp6: + continue + assert ip6.src == "2001:db8::1" + assert ip6.dst == "2001:db8:1::1" + assert hbh + assert icmp6 + found = True + assert found + + # A hop-by-hop header causes the packet to be dropped if it's not the + # first extension header + pkt = sp.IPv6(src="2001:db8::1", dst="2001:db8:1::1") \ + / sp.IPv6ExtHdrFragment(offset=0, m=0) \ + / sp.IPv6ExtHdrHopByHop() \ + / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 30)) + pkt2 = sp.IPv6(src="2001:db8::1", dst="2001:db8:1::1") \ + / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 30)) + + # Delay the send so the sniffer is running when we transmit. + ToolsHelper.print_output("/sbin/ping6 -c 1 2001:db8:1::2") + + s = DelayedSend([ pkt2, pkt ]) + replies = sp.sniff(iface=if2, timeout=10) + found = False + for p in replies: + # Expect to find the packet without the hop-by-hop header, not the + # one with + p.show() + ip6 = p.getlayer(sp.IPv6) + hbh = p.getlayer(sp.IPv6ExtHdrHopByHop) + icmp6 = p.getlayer(sp.ICMPv6EchoRequest) + + if not ip6 or not icmp6: + continue + assert ip6.src == "2001:db8::1" + assert ip6.dst == "2001:db8:1::1" + assert not hbh + assert icmp6 + found = True + assert found + +class TestFrag6_Overlap(VnetTestTemplate): + REQUIRED_MODULES = ["pf"] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1"]}, + "if1": {"prefixes6": [("2001:db8::1/64", "2001:db8::2/64")]}, + } + + def vnet2_handler(self, vnet): + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.pf_rules([ + "scrub fragment reassemble", + "pass", + ]) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_overlap(self): + "Ensure we discard packets with overlapping fragments" + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + packet = sp.IPv6(src="2001:db8::1", dst="2001:db8::2") \ + / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f00f') * 90)) + frags = sp.fragment6(packet, 128) + assert len(frags) == 3 + + f = frags[0].getlayer(sp.IPv6ExtHdrFragment) + # Fragment with overlap + overlap = sp.IPv6(src="2001:db8::1", dst="2001:db8::2") \ + / sp.IPv6ExtHdrFragment(offset = 4, m = 1, id = f.id, nh = f.nh) \ + / sp.raw(bytes.fromhex('f00f') * 4) + frags = [ frags[0], frags[1], overlap, frags[2] ] + + # Delay the send so the sniffer is running when we transmit. + s = DelayedSend(frags) + + packets = sp.sniff(iface=self.vnet.iface_alias_map["if1"].name, + timeout=3) + for p in packets: + p.show() + assert not p.getlayer(sp.ICMPv6EchoReply) + +class TestFrag6_RouteTo(VnetTestTemplate): + REQUIRED_MODULES = ["pf"] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2"]}, + "if1": {"prefixes6": [("2001:db8::1/64", "2001:db8::2/64")]}, + "if2": {"prefixes6": [("2001:db8:1::1/64", "2001:db8:1::2/64")]}, + } + + def vnet2_handler(self, vnet): + if2name = vnet.iface_alias_map["if2"].name + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.pf_rules([ + "scrub fragment reassemble", + "pass in route-to (%s 2001:db8:1::2) from 2001:db8::1 to 2001:db8:666::1" % if2name, + ]) + + ToolsHelper.print_output("/sbin/ifconfig %s mtu 1300" % if2name) + ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1") + + def vnet3_handler(self, vnet): + pass + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_too_big(self): + ToolsHelper.print_output("/sbin/route add -6 default 2001:db8::2") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + pkt = sp.IPv6(dst="2001:db8:666::1") \ + / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 3000)) + frags = sp.fragment6(pkt, 1320) + + reply = sp.sr1(frags, timeout=3) + if reply: + reply.show() + + assert reply + + ip6 = reply.getlayer(sp.IPv6) + icmp6 = reply.getlayer(sp.ICMPv6PacketTooBig) + err_ip6 = reply.getlayer(sp.IPerror6) + + assert ip6 + assert ip6.src == "2001:db8::2" + assert ip6.dst == "2001:db8::1" + assert icmp6 + assert icmp6.mtu == 1300 + assert err_ip6 + assert err_ip6.src == "2001:db8::1" + assert err_ip6.dst == "2001:db8:666::1" diff --git a/tests/sys/netpfil/pf/fragmentation_compat.sh b/tests/sys/netpfil/pf/fragmentation_compat.sh index 21ce6b734ea1..1f4550ebd69e 100644 --- a/tests/sys/netpfil/pf/fragmentation_compat.sh +++ b/tests/sys/netpfil/pf/fragmentation_compat.sh @@ -74,7 +74,7 @@ v6_head() { atf_set descr 'IPv6 fragmentation test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() @@ -230,7 +230,7 @@ overreplace_head() { atf_set descr 'ping fragment that overlaps fragment at index boundary and replace it' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overreplace_body() @@ -248,7 +248,7 @@ overindex_head() { atf_set descr 'ping fragment that overlaps the first fragment at index boundary' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overindex_body() @@ -266,7 +266,7 @@ overlimit_head() { atf_set descr 'ping fragment at index boundary that cannot be requeued' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overlimit_body() @@ -326,53 +326,6 @@ reassemble_cleanup() pft_cleanup } -atf_test_case "no_df" "cleanup" -no_df_head() -{ - atf_set descr 'Test removing of DF flag' - atf_set require.user root -} - -no_df_body() -{ - setup_router_server_ipv4 - - ifconfig ${epair_tester}a mtu 9000 - jexec router ifconfig ${epair_tester}b mtu 9000 - jexec router ifconfig ${epair_server}a mtu 1500 - jexec server ifconfig ${epair_server}b mtu 1500 - - # Sanity check. - ping_server_check_reply exit:0 --ping-type=icmp - - pft_set_rules router \ - "scrub fragment reassemble" \ - "pass out" \ - "block in" \ - "pass in inet proto icmp all icmp-type echoreq" - - # Ping with normal, fragmentable packets. - ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 - - # Ping with non-fragmentable packets, this will fail. - ping_server_check_reply exit:1 --ping-type=icmp --send-length=2000 --send-flags DF - - pft_set_rules router \ - "scrub any reassemble" \ - "pass out" \ - "block in" \ - "pass in inet proto icmp all icmp-type echoreq" - - # Ping with non-fragmentable packets again. - # This time pf will strip the DF flag. - ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 --send-flags DF -} - -no_df_cleanup() -{ - pft_cleanup -} - atf_init_test_cases() { atf_add_test_case "too_many_fragments" diff --git a/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh b/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh index fb5c15ac2ff8..7cab89f5debb 100644 --- a/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh +++ b/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh @@ -32,7 +32,7 @@ match_full_v4_head() { atf_set descr 'Matching non-fragmented IPv4 packets' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } match_full_v4_body() @@ -63,7 +63,7 @@ match_fragment_v4_head() { atf_set descr 'Matching fragmented IPv4 packets' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } match_fragment_v4_body() @@ -93,7 +93,7 @@ compat_override_v4_head() { atf_set descr 'Scrub rules override "set reassemble" for IPv4' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } compat_override_v4_body() diff --git a/tests/sys/netpfil/pf/fragmentation_pass.sh b/tests/sys/netpfil/pf/fragmentation_pass.sh index d505accba5f2..5deaba18301d 100644 --- a/tests/sys/netpfil/pf/fragmentation_pass.sh +++ b/tests/sys/netpfil/pf/fragmentation_pass.sh @@ -77,7 +77,7 @@ v6_head() { atf_set descr 'IPv6 fragmentation test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() @@ -155,6 +155,75 @@ v6_cleanup() pft_cleanup } +atf_test_case "v6_route_to" "cleanup" +v6_route_to_head() +{ + atf_set descr 'Test IPv6 reassembly combined with route-to' + atf_set require.user root +} + +v6_route_to_body() +{ + pft_init + + epair_send=$(vnet_mkepair) + epair_link=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair_send}b ${epair_link}a + vnet_mkjail singsing ${epair_link}b + + ifconfig ${epair_send}a inet6 2001:db8:42::1/64 no_dad up + + jexec alcatraz ifconfig ${epair_send}b inet6 2001:db8:42::2/64 no_dad up + jexec alcatraz ifconfig ${epair_link}a inet6 2001:db8:43::2/64 no_dad up + jexec alcatraz sysctl net.inet6.ip6.forwarding=1 + + jexec singsing ifconfig ${epair_link}b inet6 2001:db8:43::3/64 no_dad up + jexec singsing route add -6 2001:db8:42::/64 2001:db8:43::2 + route add -6 2001:db8:43::/64 2001:db8:42::2 + + jexec alcatraz ifconfig ${epair_send}b inet6 -ifdisabled + jexec alcatraz ifconfig ${epair_link}a inet6 -ifdisabled + jexec singsing ifconfig ${epair_link}b inet6 -ifdisabled + ifconfig ${epair_send}a inet6 -ifdisabled + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "set reassemble yes" \ + "pass" \ + "pass in route-to (${epair_link}a 2001:db8:43::3) inet6 proto icmp6 from any to 2001:db8:43::3 keep state" + + # Forwarding test + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 2001:db8:43::3 + + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 -s 4500 2001:db8:43::3 + + atf_check -s exit:0 -o ignore\ + ping -6 -c 1 -b 70000 -s 65000 2001:db8:43::3 + + # Now test this without fragmentation + pft_set_rules alcatraz \ + "set reassemble no" \ + "pass" \ + "pass in route-to (${epair_link}a 2001:db8:43::3) inet6 proto icmp6 from any to 2001:db8:43::3 keep state" + + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 2001:db8:43::3 + + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 -s 4500 2001:db8:43::3 + + atf_check -s exit:0 -o ignore\ + ping -6 -c 1 -b 70000 -s 65000 2001:db8:43::3 +} + +v6_route_to_cleanup() +{ + pft_cleanup +} + atf_test_case "mtu_diff" "cleanup" mtu_diff_head() { @@ -236,7 +305,7 @@ overreplace_head() { atf_set descr 'ping fragment that overlaps fragment at index boundary and replace it' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overreplace_body() @@ -254,7 +323,7 @@ overindex_head() { atf_set descr 'ping fragment that overlaps the first fragment at index boundary' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overindex_body() @@ -272,7 +341,7 @@ overlimit_head() { atf_set descr 'ping fragment at index boundary that cannot be requeued' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overlimit_body() @@ -285,6 +354,42 @@ overlimit_cleanup() pft_cleanup } +atf_test_case "overhole" "cleanup" +overhole_head() +{ + atf_set descr 'ping fragment at index boundary which modifies pf hole counter' + atf_set require.user root + atf_set require.progs python3 scapy +} + +overhole_body() +{ + frag_common overhole +} + +overhole_cleanup() +{ + pft_cleanup +} + +atf_test_case "adjhole" "cleanup" +adjhole_head() +{ + atf_set descr 'overlapping ping fragments which modifies pf hole counter' + atf_set require.user root + atf_set require.progs python3 scapy +} + +adjhole_body() +{ + frag_common adjhole +} + +adjhole_cleanup() +{ + pft_cleanup +} + atf_test_case "reassemble" "cleanup" reassemble_head() { @@ -338,61 +443,7 @@ no_df_head() { atf_set descr 'Test removing of DF flag' atf_set require.user root -} - -no_df_body() -{ - setup_router_server_ipv4 - - ifconfig ${epair_tester}a mtu 9000 - jexec router ifconfig ${epair_tester}b mtu 9000 - jexec router ifconfig ${epair_server}a mtu 1500 - jexec server ifconfig ${epair_server}b mtu 1500 - - # Sanity check. - ping_server_check_reply exit:0 --ping-type=icmp - - pft_set_rules router \ - "set reassemble no" \ - "pass out" \ - "block in" \ - "pass in inet proto icmp all icmp-type echoreq" - - # Ping with normal, fragmentable packets. - ping_server_check_reply exit:1 --ping-type=icmp --send-length=2000 - - pft_set_rules router \ - "set reassemble yes" \ - "pass out" \ - "block in" \ - "pass in inet proto icmp all icmp-type echoreq" - - # Ping with normal, fragmentable packets. - ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 - - # Ping with non-fragmentable packets. - ping_server_check_reply exit:1 --ping-type=icmp --send-length=2000 --send-flags DF - - pft_set_rules router \ - "set reassemble yes no-df" \ - "pass out" \ - "block in" \ - "pass in inet proto icmp all icmp-type echoreq" - - # Ping with non-fragmentable packets again. - # This time pf will strip the DF flag. - ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 --send-flags DF -} -no_df_cleanup() -{ - pft_cleanup -} - -atf_test_case "no_df" "cleanup" -no_df_head() -{ - atf_set descr 'Test removing of DF flag' - atf_set require.user root + atf_set require.progs python3 scapy } no_df_body() @@ -420,6 +471,7 @@ no_df_body() # getting properly forwarded. ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 --send-flags DF } + no_df_cleanup() { pft_cleanup @@ -430,6 +482,7 @@ reassemble_slowpath_head() { atf_set descr 'Test reassembly on the slow path' atf_set require.user root + atf_set require.progs python3 scapy } reassemble_slowpath_body() @@ -553,17 +606,63 @@ dummynet_nat_cleanup() pft_cleanup } +atf_test_case "dummynet_fragmented" "cleanup" +dummynet_fragmented_head() +{ + atf_set descr 'Test dummynet on NATed fragmented traffic' + atf_set require.user root + atf_set require.progs python3 scapy +} + +dummynet_fragmented_body() +{ + pft_init + dummynet_init + + # No test for IPv6. IPv6 fragment reassembly can't be disabled. + setup_router_dummy_ipv4 + + jexec router dnctl pipe 1 config delay 1 + + pft_set_rules router \ + "set reassemble no" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b inet proto udp dnpipe (1, 1)" \ + "pass out on ${epair_server}a inet proto udp" \ + + ping_dummy_check_request exit:0 --ping-type=udp --send-length=10000 --send-frag-length=1280 + + rules=$(mktemp) || exit 1 + jexec router pfctl -qvsr | normalize_pfctl_s > $rules + + # Count that fragmented packets have hit the rule only once and that + # they have not created states. There is no stateful firewall support + # for fragmented packets. + grep -qE 'pass in on epair0b inet proto udp all keep state dnpipe\(1, 1\) .* Packets: 8 Bytes: 10168 States: 0 ' $rules || + atf_fail "Fragmented packets not counted correctly" +} + +dummynet_fragmented_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "too_many_fragments" atf_add_test_case "v6" + atf_add_test_case "v6_route_to" atf_add_test_case "mtu_diff" atf_add_test_case "overreplace" atf_add_test_case "overindex" atf_add_test_case "overlimit" + atf_add_test_case "overhole" + atf_add_test_case "adjhole" atf_add_test_case "reassemble" atf_add_test_case "no_df" atf_add_test_case "reassemble_slowpath" atf_add_test_case "dummynet" atf_add_test_case "dummynet_nat" + atf_add_test_case "dummynet_fragmented" } diff --git a/tests/sys/netpfil/pf/get_state.sh b/tests/sys/netpfil/pf/get_state.sh index 35adf64f6b11..eb2bc854c800 100644 --- a/tests/sys/netpfil/pf/get_state.sh +++ b/tests/sys/netpfil/pf/get_state.sh @@ -33,7 +33,7 @@ many_head() { atf_set descr 'Test retrieving many states' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } many_body() diff --git a/tests/sys/netpfil/pf/header.py b/tests/sys/netpfil/pf/header.py new file mode 100644 index 000000000000..a5e36bc85d14 --- /dev/null +++ b/tests/sys/netpfil/pf/header.py @@ -0,0 +1,216 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Rubicon Communications, LLC (Netgate) +# +# 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. + +import pytest +import re +from utils import DelayedSend +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +class TestHeader(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1", "if2"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]}, + "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]}, + } + + def vnet2_handler(self, vnet): + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + ToolsHelper.print_output("/usr/sbin/arp -s 198.51.100.3 00:01:02:03:04:05") + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.pf_rules([ + "pass", + ]) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_too_many(self): + "Verify that we drop packets with silly numbers of headers." + + sendif = self.vnet.iface_alias_map["if1"] + recvif = self.vnet.iface_alias_map["if2"].name + gw_mac = sendif.epairb.ether + + ToolsHelper.print_output("/sbin/route add default 192.0.2.1") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Sanity check, ensure we get replies to normal ping + pkt = sp.Ether(dst=gw_mac) \ + / sp.IP(dst="198.51.100.3") \ + / sp.ICMP(type='echo-request') + s = DelayedSend(pkt, sendif.name) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + + found = False + for r in reply: + r.show() + + icmp = r.getlayer(sp.ICMP) + if not icmp: + continue + assert icmp.type == 8 # 'echo-request' + found = True + assert found + + # Up to 19 AH headers will pass + pkt = sp.Ether(dst=gw_mac) \ + / sp.IP(dst="198.51.100.3") + for i in range(0, 18): + pkt = pkt / sp.AH(nh=51, payloadlen=1) + pkt = pkt / sp.AH(nh=1, payloadlen=1) / sp.ICMP(type='echo-request') + + s = DelayedSend(pkt, sendif.name) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + found = False + for r in reply: + r.show() + + ah = r.getlayer(sp.AH) + if not ah: + continue + found = True + assert found + + # But more will get dropped + pkt = sp.Ether(dst=gw_mac) \ + / sp.IP(dst="198.51.100.3") + for i in range(0, 19): + pkt = pkt / sp.AH(nh=51, payloadlen=1) + pkt = pkt / sp.AH(nh=1, payloadlen=1) / sp.ICMP(type='echo-request') + + s = DelayedSend(pkt, sendif.name) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + + found = False + for r in reply: + r.show() + + ah = r.getlayer(sp.AH) + if not ah: + continue + found = True + assert not found + +class TestHeader6(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + SKIP_MODULES = [ "ipfilter" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1", "if2"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "if1": {"prefixes6": [("2001:db8::2/64", "2001:db8::1/64")]}, + "if2": {"prefixes6": [("2001:db8:1::2/64", "2001:db8:1::1/64")]}, + } + + def vnet2_handler(self, vnet): + ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1") + ToolsHelper.print_output("/usr/sbin/ndp -s 2001:db8:1::3 00:01:02:03:04:05") + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.pf_rules([ + "pass", + ]) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_too_many(self): + "Verify that we drop packets with silly numbers of headers." + ToolsHelper.print_output("/sbin/ifconfig") + + sendif = self.vnet.iface_alias_map["if1"] + recvif = self.vnet.iface_alias_map["if2"].name + our_mac = sendif.ether + gw_mac = sendif.epairb.ether + + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Sanity check, ensure we get replies to normal ping + pkt = sp.Ether(src=our_mac, dst=gw_mac) \ + / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3") \ + / sp.ICMPv6EchoRequest() + s = DelayedSend(pkt, sendif.name) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + + found = False + for r in reply: + r.show() + + icmp = r.getlayer(sp.ICMPv6EchoRequest) + if not icmp: + continue + found = True + assert found + + # Up to 19 AH headers will pass + pkt = sp.Ether(src=our_mac, dst=gw_mac) \ + / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3") + for i in range(0, 18): + pkt = pkt / sp.AH(nh=51, payloadlen=1) + pkt = pkt / sp.AH(nh=58, payloadlen=1) / sp.ICMPv6EchoRequest() + s = DelayedSend(pkt, sendif.name) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + + found = False + for r in reply: + r.show() + + ah = r.getlayer(sp.AH) + if not ah: + continue + found = True + assert found + + # But more will get dropped + pkt = sp.Ether(src=our_mac, dst=gw_mac) \ + / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3") + for i in range(0, 19): + pkt = pkt / sp.AH(nh=51, payloadlen=1) + pkt = pkt / sp.AH(nh=58, payloadlen=1) / sp.ICMPv6EchoRequest() + s = DelayedSend(pkt, sendif.name) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + + found = False + for r in reply: + r.show() + + ah = r.getlayer(sp.AH) + if not ah: + continue + found = True + assert not found diff --git a/tests/sys/netpfil/pf/icmp.py b/tests/sys/netpfil/pf/icmp.py new file mode 100644 index 000000000000..59f2e8190b30 --- /dev/null +++ b/tests/sys/netpfil/pf/icmp.py @@ -0,0 +1,254 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Rubicon Communications, LLC (Netgate) +# +# 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. + +import pytest +import subprocess +import re +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +def check(cmd): + ps = subprocess.Popen(cmd, shell=True) + ret = ps.wait() + if ret != 0: + raise Exception("Command %s returned %d" % (cmd, ret)) + +class TestICMP(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2"]}, + "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]}, + "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]}, + } + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + if2name = vnet.iface_alias_map["if2"].name + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "set reassemble yes", + "set state-policy if-bound", + "block", + "pass inet proto icmp icmp-type echoreq", + ]) + + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + ToolsHelper.print_output("/sbin/pfctl -x loud") + + ToolsHelper.print_output("/sbin/ifconfig %s mtu 1492" % if2name) + + def vnet3_handler(self, vnet): + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + ifname = vnet.iface_alias_map["if2"].name + ToolsHelper.print_output("/sbin/route add default 198.51.100.1") + ToolsHelper.print_output("/sbin/ifconfig %s inet alias 198.51.100.3/24" % ifname) + ToolsHelper.print_output("/sbin/ifconfig %s mtu 1492" % ifname) + + def checkfn(packet): + icmp = packet.getlayer(sp.ICMP) + if not icmp: + return False + + if icmp.type != 3: + return False + + packet.show() + return True + + sp.sniff(iface=ifname, stop_filter=checkfn) + vnet.pipe.send("Got ICMP destination unreachable packet") + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_inner_match(self): + vnet = self.vnet_map["vnet1"] + dst_vnet = self.vnet_map["vnet3"] + sendif = vnet.iface_alias_map["if1"] + + our_mac = sendif.ether + dst_mac = sendif.epairb.ether + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + ToolsHelper.print_output("/sbin/route add default 192.0.2.1") + + # Sanity check + check("/sbin/ping -c 1 192.0.2.1") + check("/sbin/ping -c 1 198.51.100.1") + check("/sbin/ping -c 2 198.51.100.3") + + # Establish a state + pkt = sp.Ether(src=our_mac, dst=dst_mac) \ + / sp.IP(src="192.0.2.2", dst="198.51.100.2") \ + / sp.ICMP(type='echo-request') \ + / "PAYLOAD" + sp.sendp(pkt, sendif.name, verbose=False) + + # Now try to pass an ICMP error message piggy-backing on that state, but + # use a different source address + pkt = sp.Ether(src=our_mac, dst=dst_mac) \ + / sp.IP(src="192.0.2.2", dst="198.51.100.3") \ + / sp.ICMP(type='dest-unreach') \ + / sp.IP(src="198.51.100.2", dst="192.0.2.2") \ + / sp.ICMP(type='echo-reply') + sp.sendp(pkt, sendif.name, verbose=False) + + try: + rcvd = self.wait_object(dst_vnet.pipe, timeout=1) + if rcvd: + raise Exception(rcvd) + except TimeoutError as e: + # We expect the timeout here. It means we didn't get the destination + # unreachable packet in vnet3 + pass + + def check_icmp_echo(self, sp, payload_size): + packet = sp.IP(dst="198.51.100.2", flags="DF") \ + / sp.ICMP(type='echo-request') \ + / sp.raw(bytes.fromhex('f0') * payload_size) + + p = sp.sr1(packet, iface=self.vnet.iface_alias_map["if1"].name, + timeout=3) + p.show() + + ip = p.getlayer(sp.IP) + icmp = p.getlayer(sp.ICMP) + assert ip + assert icmp + + if payload_size > 1464: + # Expect ICMP destination unreachable, fragmentation needed + assert ip.src == "192.0.2.1" + assert ip.dst == "192.0.2.2" + assert icmp.type == 3 # dest-unreach + assert icmp.code == 4 + assert icmp.nexthopmtu == 1492 + else: + # Expect echo reply + assert ip.src == "198.51.100.2" + assert ip.dst == "192.0.2.2" + assert icmp.type == 0 # "echo-reply" + assert icmp.code == 0 + + return + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_fragmentation_needed(self): + ToolsHelper.print_output("/sbin/route add default 192.0.2.1") + + ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.2") + ToolsHelper.print_output("/sbin/ping -c 1 -D -s 1472 198.51.100.2") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + self.check_icmp_echo(sp, 128) + self.check_icmp_echo(sp, 1464) + self.check_icmp_echo(sp, 1468) + +class TestICMP_NAT(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2"]}, + "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]}, + "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]}, + } + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + if2name = vnet.iface_alias_map["if2"].name + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "set reassemble yes", + "set state-policy if-bound", + "nat on %s inet from 192.0.2.0/24 to any -> (%s)" % (if2name, if2name), + "block", + "pass inet proto icmp icmp-type echoreq", + ]) + + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + ToolsHelper.print_output("/sbin/pfctl -x loud") + + def vnet3_handler(self, vnet): + ifname = vnet.iface_alias_map["if2"].name + ToolsHelper.print_output("/sbin/ifconfig %s inet alias 198.51.100.3/24" % ifname) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_id_conflict(self): + """ + Test ICMP echo requests with the same ID from different clients. + Windows does this, and it can confuse pf. + """ + ifname = self.vnet.iface_alias_map["if1"].name + ToolsHelper.print_output("/sbin/route add default 192.0.2.1") + ToolsHelper.print_output("/sbin/ifconfig %s inet alias 192.0.2.3/24" % ifname) + + ToolsHelper.print_output("/sbin/ping -c 1 192.0.2.1") + ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.1") + ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.2") + ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.3") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Address one + packet = sp.IP(src="192.0.2.2", dst="198.51.100.2", flags="DF") \ + / sp.ICMP(type='echo-request', id=42) \ + / sp.raw(bytes.fromhex('f0') * 16) + + p = sp.sr1(packet, timeout=3) + p.show() + ip = p.getlayer(sp.IP) + icmp = p.getlayer(sp.ICMP) + assert ip + assert icmp + assert ip.dst == "192.0.2.2" + assert icmp.id == 42 + + # Address one + packet = sp.IP(src="192.0.2.3", dst="198.51.100.2", flags="DF") \ + / sp.ICMP(type='echo-request', id=42) \ + / sp.raw(bytes.fromhex('f0') * 16) + + p = sp.sr1(packet, timeout=3) + p.show() + ip = p.getlayer(sp.IP) + icmp = p.getlayer(sp.ICMP) + assert ip + assert icmp + assert ip.dst == "192.0.2.3" + assert icmp.id == 42 diff --git a/tests/sys/netpfil/pf/icmp.sh b/tests/sys/netpfil/pf/icmp.sh index 72b531b08c51..279e3c7a29d5 100644 --- a/tests/sys/netpfil/pf/icmp.sh +++ b/tests/sys/netpfil/pf/icmp.sh @@ -33,7 +33,7 @@ cve_2019_5598_head() { atf_set descr 'Test CVE-2019-5598' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } cve_2019_5598_body() @@ -71,7 +71,74 @@ cve_2019_5598_cleanup() pft_cleanup } +atf_test_case "ttl_exceeded" "cleanup" +ttl_exceeded_head() +{ + atf_set descr 'Test that we correctly translate TTL exceeded back' + atf_set require.user root +} + +ttl_exceeded_body() +{ + pft_init + + epair_srv=$(vnet_mkepair) + epair_int=$(vnet_mkepair) + epair_cl=$(vnet_mkepair) + + vnet_mkjail srv ${epair_srv}a + jexec srv ifconfig ${epair_srv}a 192.0.2.1/24 up + jexec srv route add default 192.0.2.2 + + vnet_mkjail int ${epair_srv}b ${epair_int}a + jexec int sysctl net.inet.ip.forwarding=1 + jexec int ifconfig ${epair_srv}b 192.0.2.2/24 up + jexec int ifconfig ${epair_int}a 203.0.113.2/24 up + + vnet_mkjail nat ${epair_int}b ${epair_cl}b + jexec nat ifconfig ${epair_int}b 203.0.113.1/24 up + jexec nat ifconfig ${epair_cl}b 198.51.100.2/24 up + jexec nat sysctl net.inet.ip.forwarding=1 + jexec nat route add default 203.0.113.2 + + vnet_mkjail cl ${epair_cl}a + jexec cl ifconfig ${epair_cl}a 198.51.100.1/24 up + jexec cl route add default 198.51.100.2 + + jexec nat pfctl -e + pft_set_rules nat \ + "nat on ${epair_int}b from 198.51.100.0/24 -> (${epair_int}b)" \ + "block" \ + "pass inet proto udp" \ + "pass inet proto icmp icmp-type { echoreq }" + + # Sanity checks + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 198.51.100.2 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 203.0.113.1 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 203.0.113.2 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 192.0.2.1 + + echo "UDP" + atf_check -s exit:0 -e ignore -o match:".*203.0.113.2.*" \ + jexec cl traceroute 192.0.2.1 + jexec nat pfctl -Fs + + echo "ICMP" + atf_check -s exit:0 -e ignore -o match:".*203.0.113.2.*" \ + jexec cl traceroute -I 192.0.2.1 +} + +ttl_exceeded_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "cve_2019_5598" + atf_add_test_case "ttl_exceeded" } diff --git a/tests/sys/netpfil/pf/icmp6.sh b/tests/sys/netpfil/pf/icmp6.sh new file mode 100644 index 000000000000..c55af906e3a6 --- /dev/null +++ b/tests/sys/netpfil/pf/icmp6.sh @@ -0,0 +1,204 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Rubicon Communications, LLC (Netgate) +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +common_dir=$(atf_get_srcdir)/../common + +atf_test_case "zero_id" "cleanup" +zero_id_head() +{ + atf_set descr 'Test ICMPv6 echo with ID 0 keep being blocked' + atf_set require.user root + atf_set require.progs python3 scapy +} + +zero_id_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 2001:db8::1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "set block-policy drop" \ + "antispoof quick for { egress ${epair}b }" \ + "block all" \ + "pass out" \ + "pass in quick inet6 proto IPV6-ICMP icmp6-type 135" \ + "pass in quick inet6 proto IPV6-ICMP icmp6-type 136" \ + "pass out quick inet6 proto IPV6 from self to any" + + # Now we can't ping + atf_check -s exit:2 -o ignore \ + ping -c 1 2001:db8::1 + + # Force neighbour discovery + ndp -d 2001:db8::1 + + # Verify that we don't confuse echo request with ID 0 for neighbour discovery + atf_check -s exit:1 -o ignore \ + ${common_dir}/pft_ping.py \ + --sendif ${epair}a \ + --to 2001:db8::1 \ + --replyif ${epair}a + + jexec alcatraz pfctl -ss -vv + jexec alcatraz pfctl -sr -vv +} + +zero_id_cleanup() +{ + pft_cleanup +} + +atf_test_case "ttl_exceeded" "cleanup" +ttl_exceeded_head() +{ + atf_set descr 'Test that we correctly translate TTL exceeded back' + atf_set require.user root +} + +ttl_exceeded_body() +{ + pft_init + + epair_srv=$(vnet_mkepair) + epair_int=$(vnet_mkepair) + epair_cl=$(vnet_mkepair) + + vnet_mkjail srv ${epair_srv}a + jexec srv ifconfig ${epair_srv}a inet6 2001:db8:1::1/64 no_dad up + jexec srv route add -6 default 2001:db8:1::2 + + vnet_mkjail int ${epair_srv}b ${epair_int}a + jexec int sysctl net.inet6.ip6.forwarding=1 + jexec int ifconfig ${epair_srv}b inet6 2001:db8:1::2/64 no_dad up + jexec int ifconfig ${epair_int}a inet6 2001:db8:2::2/64 no_dad up + + vnet_mkjail nat ${epair_int}b ${epair_cl}b + jexec nat ifconfig ${epair_int}b inet6 2001:db8:2::1 no_dad up + jexec nat ifconfig ${epair_cl}b inet6 2001:db8:3::2/64 no_dad up + jexec nat sysctl net.inet6.ip6.forwarding=1 + jexec nat route add -6 default 2001:db8:2::2 + + vnet_mkjail cl ${epair_cl}a + jexec cl ifconfig ${epair_cl}a inet6 2001:db8:3::1/64 no_dad up + jexec cl route add -6 default 2001:db8:3::2 + + jexec nat pfctl -e + pft_set_rules nat \ + "nat on ${epair_int}b from 2001:db8:3::/64 -> (${epair_int}b:0)" \ + "block" \ + "pass inet6 proto udp" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv, echoreq }" + + # Sanity checks + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 2001:db8:3::2 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 2001:db8:2::1 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 2001:db8:2::2 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 2001:db8:1::1 + + echo "UDP" + atf_check -s exit:0 -e ignore -o match:".*2001:db8:2::2.*" \ + jexec cl traceroute6 2001:db8:1::1 + jexec nat pfctl -Fs + + echo "ICMP" + atf_check -s exit:0 -e ignore -o match:".*2001:db8:2::2.*" \ + jexec cl traceroute6 -I 2001:db8:1::1 +} + +ttl_exceeded_cleanup() +{ + pft_cleanup +} + +atf_test_case "repeat" "cleanup" +repeat_head() +{ + atf_set descr 'Ensure that repeated NDs work' + atf_set require.user root + atf_set require.progs ndisc6 +} + +repeat_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 2001:db8::1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block all" \ + "pass quick inet6 proto ipv6-icmp all icmp6-type neighbrsol keep state (if-bound) ridentifier 1000000107" + + jexec alcatraz pfctl -x loud + ndisc6 -m -n -r 1 2001:db8::1 ${epair}a + jexec alcatraz pfctl -ss -vv + + atf_check -s exit:0 -o ignore \ + ndisc6 -m -n -r 1 2001:db8::1 ${epair}a + jexec alcatraz pfctl -ss -vv + atf_check -s exit:0 -o ignore \ + ndisc6 -m -n -r 1 2001:db8::1 ${epair}a + jexec alcatraz pfctl -ss -vv + atf_check -s exit:0 -o ignore \ + ndisc6 -m -n -r 1 2001:db8::1 ${epair}a + jexec alcatraz pfctl -ss -vv +} + +repeat_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "zero_id" + atf_add_test_case "ttl_exceeded" + atf_add_test_case "repeat" +} diff --git a/tests/sys/netpfil/pf/if_enc.sh b/tests/sys/netpfil/pf/if_enc.sh new file mode 100644 index 000000000000..40090b175470 --- /dev/null +++ b/tests/sys/netpfil/pf/if_enc.sh @@ -0,0 +1,178 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Igor Ostapenko <pm@igoro.pro> +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +# +# The following network is used as a base for testing. +# +# +# ${awan}b |----------| ${bwan}b +# 2.0.0.1 | host wan | 3.0.0.1 +# .---->| Internet |<----. +# A WAN | |----------| | B WAN +# | | +# Office A side | | Office B side +# | ${awan}a ${bwan}a | +# v 2.0.0.22 3.0.0.33 v +# ${alan}b |----------| |----------| ${blan}b +# 1.0.0.1 | host agw | | host bgw | 4.0.0.1 +# .----------->| gateway | < IPsec > | gateway |<-----------. +# | A LAN |----------| tunnel |----------| B LAN | +# | | +# | | +# | ${alan}a ${blan}a | +# v 1.0.0.11 4.0.0.44 v +# |----------| |----------| +# | host a | | host b | +# | client | | client | +# |----------| |----------| +# +# +# There is routing between office A clients and office B ones. The traffic is +# encrypted, i.e. host wan should see IPsec flow (ESP packets). +# + +ipsec_init() +{ + if ! sysctl -q kern.features.ipsec >/dev/null ; then + atf_skip "This test requires ipsec" + fi +} + +if_enc_init() +{ + ipsec_init + if ! kldstat -q -m if_enc; then + atf_skip "This test requires if_enc" + fi +} + +build_test_network() +{ + alan=$(vnet_mkepair) + awan=$(vnet_mkepair) + bwan=$(vnet_mkepair) + blan=$(vnet_mkepair) + + # host a + vnet_mkjail a ${alan}a + jexec a ifconfig ${alan}a 1.0.0.11/24 up + jexec a route add default 1.0.0.1 + + # host agw + vnet_mkjail agw ${alan}b ${awan}a + jexec agw ifconfig ${alan}b 1.0.0.1/24 up + jexec agw ifconfig ${awan}a 2.0.0.22/24 up + jexec agw route add default 2.0.0.1 + jexec agw sysctl net.inet.ip.forwarding=1 + + # host wan + vnet_mkjail wan ${awan}b ${bwan}b + jexec wan ifconfig ${awan}b 2.0.0.1/24 up + jexec wan ifconfig ${bwan}b 3.0.0.1/24 up + jexec wan sysctl net.inet.ip.forwarding=1 + + # host bgw + vnet_mkjail bgw ${bwan}a ${blan}b + jexec bgw ifconfig ${bwan}a 3.0.0.33/24 up + jexec bgw ifconfig ${blan}b 4.0.0.1/24 up + jexec bgw route add default 3.0.0.1 + jexec bgw sysctl net.inet.ip.forwarding=1 + + # host b + vnet_mkjail b ${blan}a + jexec b ifconfig ${blan}a 4.0.0.44/24 up + jexec b route add default 4.0.0.1 + + # Office A VPN setup + echo ' + spdadd 1.0.0.0/24 4.0.0.0/24 any -P out ipsec esp/tunnel/2.0.0.22-3.0.0.33/require; + spdadd 4.0.0.0/24 1.0.0.0/24 any -P in ipsec esp/tunnel/3.0.0.33-2.0.0.22/require; + add 2.0.0.22 3.0.0.33 esp 0x203 -E aes-gcm-16 "123456789012345678901234567890123456"; + add 3.0.0.33 2.0.0.22 esp 0x302 -E aes-gcm-16 "123456789012345678901234567890123456"; + ' | jexec agw setkey -c + + # Office B VPN setup + echo ' + spdadd 4.0.0.0/24 1.0.0.0/24 any -P out ipsec esp/tunnel/3.0.0.33-2.0.0.22/require; + spdadd 1.0.0.0/24 4.0.0.0/24 any -P in ipsec esp/tunnel/2.0.0.22-3.0.0.33/require; + add 2.0.0.22 3.0.0.33 esp 0x203 -E aes-gcm-16 "123456789012345678901234567890123456"; + add 3.0.0.33 2.0.0.22 esp 0x302 -E aes-gcm-16 "123456789012345678901234567890123456"; + ' | jexec bgw setkey -c +} + +atf_test_case "ip4_pfil_in_after_stripping" "cleanup" +ip4_pfil_in_after_stripping_head() +{ + atf_set descr 'Test that pf pulls up mbuf if m_len==0 after stripping the outer header' + atf_set require.user root + atf_set require.progs nc +} +ip4_pfil_in_after_stripping_body() +{ + pft_init + if_enc_init + + build_test_network + + # Sanity check + atf_check -s exit:0 -o ignore jexec a ping -c3 4.0.0.44 + + # Configure port forwarding on host bgw + jexec bgw ifconfig enc0 up + jexec bgw sysctl net.inet.ipsec.filtertunnel=0 + jexec bgw sysctl net.enc.in.ipsec_filter_mask=2 # after stripping + jexec bgw sysctl net.enc.out.ipsec_filter_mask=1 # before outer header + jexec bgw pfctl -e + pft_set_rules bgw \ + "rdr on enc0 proto tcp to 4.0.0.1 port 666 -> 4.0.0.44" \ + "pass" + + # Prepare the catcher on host b + echo "unexpected" > ./receiver + jexec b nc -n4l -N 666 > ./receiver & + nc_pid=$! + sleep 1 + + # Poke it from host a to host bgw + spell="Ak Ohum Oktay Weez Barsoom." + echo $spell | jexec a nc -w3 4.0.0.1 666 + + # Expect it to hit host b instead + sleep 1 # let the catcher finish + jexec b kill -KILL $nc_pid # in a fail case the catcher may listen forever + atf_check_equal "$spell" "$(cat ./receiver)" +} +ip4_pfil_in_after_stripping_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "ip4_pfil_in_after_stripping" +} diff --git a/tests/sys/netpfil/pf/ioctl/Makefile b/tests/sys/netpfil/pf/ioctl/Makefile index 5d4e9dbcff5f..6bcf48432d30 100644 --- a/tests/sys/netpfil/pf/ioctl/Makefile +++ b/tests/sys/netpfil/pf/ioctl/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netpfil/pf/ioctl diff --git a/tests/sys/netpfil/pf/ioctl/validation.c b/tests/sys/netpfil/pf/ioctl/validation.c index 1ce8999dcb91..18fafe11c6ab 100644 --- a/tests/sys/netpfil/pf/ioctl/validation.c +++ b/tests/sys/netpfil/pf/ioctl/validation.c @@ -32,6 +32,7 @@ #include <net/if.h> #include <net/pfvar.h> +#include <errno.h> #include <fcntl.h> #include <stdio.h> @@ -893,6 +894,39 @@ ATF_TC_CLEANUP(rpool_mtx2, tc) COMMON_CLEANUP(); } +ATF_TC_WITH_CLEANUP(natlook); +ATF_TC_HEAD(natlook, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} + +ATF_TC_BODY(natlook, tc) +{ + struct pfioc_natlook nl = { 0 }; + + COMMON_HEAD(); + + nl.af = AF_INET; + nl.proto = IPPROTO_ICMP; + nl.saddr.v4.s_addr = 0x01020304; + nl.daddr.v4.s_addr = 0x05060708; + + /* Invalid direction */ + nl.direction = 42; + + ATF_CHECK_ERRNO(EINVAL, ioctl(dev, DIOCNATLOOK, &nl) == -1); + + /* Invalid af */ + nl.direction = PF_IN; + nl.af = 99; + + ATF_CHECK_ERRNO(EAFNOSUPPORT, ioctl(dev, DIOCNATLOOK, &nl) == -1); +} + +ATF_TC_CLEANUP(natlook, tc) +{ + COMMON_CLEANUP(); +} ATF_TP_ADD_TCS(tp) { @@ -918,6 +952,7 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, tag); ATF_TP_ADD_TC(tp, rpool_mtx); ATF_TP_ADD_TC(tp, rpool_mtx2); + ATF_TP_ADD_TC(tp, natlook); return (atf_no_error()); } diff --git a/tests/sys/netpfil/pf/killstate.sh b/tests/sys/netpfil/pf/killstate.sh index 046d640ed355..447a4e388f11 100644 --- a/tests/sys/netpfil/pf/killstate.sh +++ b/tests/sys/netpfil/pf/killstate.sh @@ -47,7 +47,7 @@ v4_head() { atf_set descr 'Test killing states by IPv4 address' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v4_body() @@ -110,7 +110,7 @@ v6_head() { atf_set descr 'Test killing states by IPv6 address' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() @@ -177,7 +177,7 @@ label_head() { atf_set descr 'Test killing states by label' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } label_body() @@ -241,7 +241,7 @@ multilabel_head() { atf_set descr 'Test killing states with multiple labels by label' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } multilabel_body() @@ -321,7 +321,7 @@ gateway_head() { atf_set descr 'Test killing states by route-to/reply-to address' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } gateway_body() @@ -410,7 +410,7 @@ match_body() vnet_mkjail singsing ${epair_two}b jexec singsing ifconfig ${epair_two}b 198.51.100.2/24 up jexec singsing route add default 198.51.100.1 - jexec singsing /usr/sbin/inetd -p inetd-echo.pid \ + jexec singsing /usr/sbin/inetd -p ${PWD}/inetd-echo.pid \ $(atf_get_srcdir)/echo_inetd.conf route add 198.51.100.0/24 192.0.2.2 @@ -462,7 +462,7 @@ interface_head() { atf_set descr 'Test killing states based on interface' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } interface_body() @@ -518,7 +518,7 @@ id_head() { atf_set descr 'Test killing states by id' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } id_body() @@ -574,12 +574,68 @@ id_cleanup() pft_cleanup } +atf_test_case "key" "cleanup" +key_head() +{ + atf_set descr 'Test killing states by their key' + atf_set require.user root + atf_set require.progs python3 scapy +} + +key_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a 192.0.2.1/24 up + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz pfctl -e + + pft_set_rules alcatraz \ + "block all" \ + "pass in proto tcp" \ + "pass in proto icmp" + + # Sanity check & establish state + atf_check -s exit:0 -o ignore ${common_dir}/pft_ping.py \ + --sendif ${epair}a \ + --to 192.0.2.2 \ + --replyif ${epair}a + + # Get the state key + key=$(jexec alcatraz pfctl -ss -vvv | awk '/icmp/ { print($2 " " $3 " " $4 " " $5); }') + bad_key=$(echo ${key} | sed 's/icmp/tcp/') + + # Kill the wrong key + atf_check -s exit:0 -e "match:killed 0 states" \ + jexec alcatraz pfctl -k key -k "${bad_key}" + if ! find_state; + then + atf_fail "Killing a different ID removed the state." + fi + + # Kill the correct key + atf_check -s exit:0 -e "match:killed 1 states" \ + jexec alcatraz pfctl -k key -k "${key}" + if find_state; + then + atf_fail "Killing the state did not remove it." + fi +} + +key_cleanup() +{ + pft_cleanup +} + atf_test_case "nat" "cleanup" nat_head() { atf_set descr 'Test killing states by their NAT-ed IP address' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } nat_body() @@ -653,5 +709,6 @@ atf_init_test_cases() atf_add_test_case "match" atf_add_test_case "interface" atf_add_test_case "id" + atf_add_test_case "key" atf_add_test_case "nat" } diff --git a/tests/sys/netpfil/pf/limits.sh b/tests/sys/netpfil/pf/limits.sh new file mode 100644 index 000000000000..69f0b6af2ccf --- /dev/null +++ b/tests/sys/netpfil/pf/limits.sh @@ -0,0 +1,119 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Kristof Provost <kp@FreeBSD.org> +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Test setting and retrieving limits' + atf_set require.user root +} + +basic_body() +{ + pft_init + + vnet_mkjail alcatraz + + pft_set_rules alcatraz \ + "set limit states 200" \ + "set limit frags 100" \ + "set limit src-nodes 50" \ + "set limit table-entries 25" + + atf_check -s exit:0 -o match:'states.*200' \ + jexec alcatraz pfctl -sm + atf_check -s exit:0 -o match:'frags.*100' \ + jexec alcatraz pfctl -sm + atf_check -s exit:0 -o match:'src-nodes.*50' \ + jexec alcatraz pfctl -sm + atf_check -s exit:0 -o match:'table-entries.*25' \ + jexec alcatraz pfctl -sm +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_test_case "zero" "cleanup" +zero_head() +{ + atf_set descr 'Test changing a limit from zero on an in-use zone' + atf_set require.user root +} + +zero_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + atf_check -s exit:0 -o ignore \ + ping -c 3 192.0.2.1 + + jexec alcatraz pfctl -e + # Set no limit + pft_set_rules noflush alcatraz \ + "set limit states 0" \ + "pass" + + # Check that we really report no limit + atf_check -s exit:0 -o 'match:states hard limit 0' \ + jexec alcatraz pfctl -sa + + # Create a state + atf_check -s exit:0 -o ignore \ + ping -c 3 192.0.2.1 + + # Limit states + pft_set_rules noflush alcatraz \ + "set limit states 1000" \ + "pass" + + # And create a new state + atf_check -s exit:0 -o ignore \ + ping -c 3 192.0.2.1 + + atf_check -s exit:0 -o 'match:states hard limit 1000' \ + jexec alcatraz pfctl -sa +} + +zero_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" + atf_add_test_case "zero" +} diff --git a/tests/sys/netpfil/pf/map_e.sh b/tests/sys/netpfil/pf/map_e.sh deleted file mode 100644 index 742264dcf547..000000000000 --- a/tests/sys/netpfil/pf/map_e.sh +++ /dev/null @@ -1,91 +0,0 @@ -# -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (c) 2021 KUROSAWA Takahiro <takahiro.kurosawa@gmail.com> -# -# 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. - -. $(atf_get_srcdir)/utils.subr - -atf_test_case "map_e" "cleanup" -map_e_head() -{ - atf_set descr 'map-e-portset test' - atf_set require.user root -} - -map_e_body() -{ - NC_TRY_COUNT=12 - - pft_init - - epair_map_e=$(vnet_mkepair) - epair_echo=$(vnet_mkepair) - - vnet_mkjail map_e ${epair_map_e}b ${epair_echo}a - vnet_mkjail echo ${epair_echo}b - - ifconfig ${epair_map_e}a 192.0.2.2/24 up - route add -net 198.51.100.0/24 192.0.2.1 - - jexec map_e ifconfig ${epair_map_e}b 192.0.2.1/24 up - jexec map_e ifconfig ${epair_echo}a 198.51.100.1/24 up - jexec map_e sysctl net.inet.ip.forwarding=1 - - jexec echo ifconfig ${epair_echo}b 198.51.100.2/24 up - jexec echo /usr/sbin/inetd -p inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf - - # Enable pf! - jexec map_e pfctl -e - pft_set_rules map_e \ - "nat pass on ${epair_echo}a inet from 192.0.2.0/24 to any -> (${epair_echo}a) map-e-portset 2/12/0x342" - - # Only allow specified ports. - jexec echo pfctl -e - pft_set_rules echo "block return all" \ - "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 19720:19723 to (${epair_echo}b) port 7" \ - "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 36104:36107 to (${epair_echo}b) port 7" \ - "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 52488:52491 to (${epair_echo}b) port 7" \ - "set skip on lo" - - i=0 - while [ ${i} -lt ${NC_TRY_COUNT} ] - do - echo "foo ${i}" | timeout 2 nc -N 198.51.100.2 7 - if [ $? -ne 0 ]; then - atf_fail "nc failed (${i})" - fi - i=$((${i}+1)) - done -} - -map_e_cleanup() -{ - rm -f inetd-echo.pid - pft_cleanup -} - -atf_init_test_cases() -{ - atf_add_test_case "map_e" -} diff --git a/tests/sys/netpfil/pf/match.sh b/tests/sys/netpfil/pf/match.sh index bb088c5bf47c..58c1e021310a 100644 --- a/tests/sys/netpfil/pf/match.sh +++ b/tests/sys/netpfil/pf/match.sh @@ -26,6 +26,8 @@ . $(atf_get_srcdir)/utils.subr +common_dir=$(atf_get_srcdir)/../common + atf_test_case "dummynet" "cleanup" dummynet_head() { @@ -67,7 +69,117 @@ dummynet_cleanup() pft_cleanup } +atf_test_case "quick" "cleanup" +quick_head() +{ + atf_set descr 'Test quick on match rules' + atf_set require.user root +} + +quick_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}b + + ifconfig ${epair}a 192.0.2.1/24 up + jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.2 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass" \ + "match in quick proto icmp" \ + "block" + + # 'match quick' should retain the previous pass/block state + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.2 + + pft_set_rules alcatraz \ + "block" \ + "match in quick proto icmp" \ + "pass" + + atf_check -s exit:2 -o ignore \ + ping -c 1 192.0.2.2 +} + +quick_cleanup() +{ + pft_cleanup +} + +atf_test_case "allow_opts" "cleanup" +allow_opts_head() +{ + atf_set descr 'Test allowing IP options via match' + atf_set require.user root + atf_set require.progs python3 scapy +} + +allow_opts_body() +{ + pft_init + + epair=$(vnet_mkepair) + + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + jexec alcatraz pfctl -e + jexec alcatraz pfctl -x loud + pft_set_rules alcatraz \ + "match proto icmp allow-opts" \ + "pass" + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + atf_check -s exit:0 -o ignore \ + ${common_dir}/pft_ping.py \ + --sendif ${epair}b \ + --to 192.0.2.1 \ + --send-nop \ + --replyif ${epair}b + + # This doesn't work without 'allow-opts' + pft_set_rules alcatraz \ + "match proto icmp" \ + "pass" + atf_check -s exit:1 -o ignore \ + ${common_dir}/pft_ping.py \ + --sendif ${epair}b \ + --to 192.0.2.1 \ + --send-nop \ + --replyif ${epair}b + + # Setting it on a pass rule still works. + pft_set_rules alcatraz \ + "pass allow-opts" + atf_check -s exit:0 -o ignore \ + ${common_dir}/pft_ping.py \ + --sendif ${epair}b \ + --to 192.0.2.1 \ + --send-nop \ + --replyif ${epair}b +} + +allow_opts_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "dummynet" + atf_add_test_case "quick" + atf_add_test_case "allow_opts" } diff --git a/tests/sys/netpfil/pf/max_pkt_rate.sh b/tests/sys/netpfil/pf/max_pkt_rate.sh new file mode 100644 index 000000000000..bdd140eb60dd --- /dev/null +++ b/tests/sys/netpfil/pf/max_pkt_rate.sh @@ -0,0 +1,121 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Rubicon Communications, LLC (Netgate) +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +common_setup() +{ + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b inet 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e +} + +common_test() +{ + # One ping will pass + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + # As will a second + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + # But the third should fail + atf_check -s exit:2 -o ignore \ + ping -c 1 192.0.2.1 + + # But three seconds later we can ping again + sleep 3 + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 +} + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Basic maximum packet rate test' + atf_set require.user root +} + +basic_body() +{ + pft_init + + common_setup + + pft_set_rules alcatraz \ + "block" \ + "pass in proto icmp max-pkt-rate 2/2" + + common_test +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_test_case "anchor" "cleanup" +anchor_head() +{ + atf_set descr 'maximum packet rate on anchor' + atf_set require.user root +} + +anchor_body() +{ + pft_init + + common_setup + + pft_set_rules alcatraz \ + "block" \ + "anchor \"foo\" proto icmp max-pkt-rate 2/2 {\n \ + pass \n \ + }" + + common_test +} + +anchor_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" + atf_add_test_case "anchor" +} diff --git a/tests/sys/netpfil/pf/max_pkt_size.sh b/tests/sys/netpfil/pf/max_pkt_size.sh new file mode 100644 index 000000000000..030d642303fc --- /dev/null +++ b/tests/sys/netpfil/pf/max_pkt_size.sh @@ -0,0 +1,122 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Rubicon Communications, LLC (Netgate) +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +common_setup() +{ + epair=$(vnet_mkepair) + + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + jexec alcatraz pfctl -e +} + +common_test() +{ + # Small packets pass + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 -s 100 192.0.2.1 + + # Larger packets do not + atf_check -s exit:2 -o ignore \ + ping -c 3 -s 101 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 3 -s 128 192.0.2.1 +} + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Basic max-pkt-size test' + atf_set require.user root +} + +basic_body() +{ + pft_init + + common_setup + + pft_set_rules alcatraz \ + "pass max-pkt-size 128" + + common_test + + # We can enforce this on fragmented packets too + pft_set_rules alcatraz \ + "pass max-pkt-size 2000" + + atf_check -s exit:0 -o ignore \ + ping -c 1 -s 1400 192.0.2.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 -s 1972 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 1 -s 1973 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 3 -s 3000 192.0.2.1 +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_test_case "match" "cleanup" +match_head() +{ + atf_set descr 'max-pkt-size on match rules' + atf_set require.user root +} + +match_body() +{ + pft_init + + common_setup + + pft_set_rules alcatraz \ + "match in max-pkt-size 128" \ + "pass" + + common_test +} + +match_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" + atf_add_test_case "match" +} diff --git a/tests/sys/netpfil/pf/max_states.sh b/tests/sys/netpfil/pf/max_states.sh new file mode 100755 index 000000000000..1bf6814f9283 --- /dev/null +++ b/tests/sys/netpfil/pf/max_states.sh @@ -0,0 +1,62 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Kajetan Staszkiewicz <vegeta@tuxpowered.net> +# +# 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. + +. $(atf_get_srcdir)/utils.subr + + +max_states_head() +{ + atf_set descr 'Max states per rule' + atf_set require.user root + atf_set require.progs python3 scapy +} + +max_states_body() +{ + setup_router_dummy_ipv6 + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b inet6 proto tcp keep state (max 3)" \ + "pass out on ${epair_server}a inet6 proto tcp keep state" + + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4201 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4202 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4203 + ping_dummy_check_request exit:1 --ping-type=tcpsyn --send-sport=4204 +} + +max_states_cleanup() +{ + pft_cleanup +} + + +atf_init_test_cases() +{ + atf_add_test_case "max_states" +} diff --git a/tests/sys/netpfil/pf/mbuf.sh b/tests/sys/netpfil/pf/mbuf.sh new file mode 100644 index 000000000000..d845f793a969 --- /dev/null +++ b/tests/sys/netpfil/pf/mbuf.sh @@ -0,0 +1,236 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Igor Ostapenko <pm@igoro.pro> +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +dummymbuf_init() +{ + if ! kldstat -q -m dummymbuf; then + atf_skip "This test requires dummymbuf" + fi +} + +atf_test_case "inet_in_mbuf_len" "cleanup" +inet_in_mbuf_len_head() +{ + atf_set descr 'Test that pf can handle inbound with the first mbuf with m_len < sizeof(struct ip)' + atf_set require.user root +} +inet_in_mbuf_len_body() +{ + pft_init + dummymbuf_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a 192.0.2.1/24 up + + # Set up a simple jail with one interface + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + # Should be denied + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 + + # Should be allowed by from/to addresses + pft_set_rules alcatraz \ + "block" \ + "pass in from 192.0.2.1 to 192.0.2.2" + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + # Should still work for m_len=0 + jexec alcatraz pfilctl link -i dummymbuf:inet inet + jexec alcatraz sysctl net.dummymbuf.rules="inet in ${epair}b pull-head 0;" + atf_check_equal "0" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=1 + jexec alcatraz sysctl net.dummymbuf.rules="inet in ${epair}b pull-head 1;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=19 + # provided IPv4 basic header is 20 bytes long, it should impact the dst addr + jexec alcatraz sysctl net.dummymbuf.rules="inet in ${epair}b pull-head 19;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" +} +inet_in_mbuf_len_cleanup() +{ + pft_cleanup +} + +atf_test_case "inet6_in_mbuf_len" "cleanup" +inet6_in_mbuf_len_head() +{ + atf_set descr 'Test that pf can handle inbound with the first mbuf with m_len < sizeof(struct ip6_hdr)' + atf_set require.user root +} +inet6_in_mbuf_len_body() +{ + pft_init + dummymbuf_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a inet6 2001:db8::1/64 up no_dad + + # Set up a simple jail with one interface + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b inet6 2001:db8::2/64 up no_dad + + # Sanity check + atf_check -s exit:0 -o ignore ping -c1 2001:db8::2 + + # Should be denied + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" \ + "pass quick inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" + atf_check -s not-exit:0 -o ignore ping -c1 -t1 2001:db8::2 + + # Avoid redundant ICMPv6 packets to avoid false positives during + # counting of net.dummymbuf.hits. + ndp -i ${epair}a -- -nud + jexec alcatraz ndp -i ${epair}b -- -nud + + # Should be allowed by from/to addresses + pft_set_rules alcatraz \ + "block" \ + "pass quick inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in inet6 from 2001:db8::1 to 2001:db8::2" + atf_check -s exit:0 -o ignore ping -c1 2001:db8::2 + + # Should still work for m_len=0 + jexec alcatraz pfilctl link -i dummymbuf:inet6 inet6 + jexec alcatraz sysctl net.dummymbuf.rules="inet6 in ${epair}b pull-head 0;" + atf_check_equal "0" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + atf_check -s exit:0 -o ignore ping -c1 2001:db8::2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=1 + jexec alcatraz sysctl net.dummymbuf.rules="inet6 in ${epair}b pull-head 1;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 2001:db8::2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=39 + # provided IPv6 basic header is 40 bytes long, it should impact the dst addr + jexec alcatraz sysctl net.dummymbuf.rules="inet6 in ${epair}b pull-head 39;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 2001:db8::2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" +} +inet6_in_mbuf_len_cleanup() +{ + pft_cleanup +} + +atf_test_case "ethernet_in_mbuf_len" "cleanup" +ethernet_in_mbuf_len_head() +{ + atf_set descr 'Test that pf can handle inbound with the first mbuf with m_len < sizeof(struct ether_header)' + atf_set require.user root +} +ethernet_in_mbuf_len_body() +{ + pft_init + dummymbuf_init + + epair=$(vnet_mkepair) + epair_a_mac=$(ifconfig ${epair}a ether | awk '/ether/ { print $2; }') + ifconfig ${epair}a 192.0.2.1/24 up + + # Set up a simple jail with one interface + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up + epair_b_mac=$(jexec alcatraz ifconfig ${epair}b ether | awk '/ether/ { print $2; }') + + # Sanity check + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + # Should be denied + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "ether block" \ + "pass" + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 + + # Should be allowed by from/to addresses + echo $epair_a_mac + echo $epair_b_mac + pft_set_rules alcatraz \ + "ether block" \ + "ether pass in from ${epair_a_mac} to ${epair_b_mac}" \ + "ether pass out from ${epair_b_mac} to ${epair_a_mac}" \ + "pass" + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + # Should still work for m_len=0 + jexec alcatraz pfilctl link -i dummymbuf:ethernet ethernet + jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 0;" + atf_check_equal "0" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=1 + jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 1;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=11 + # for the simplest L2 Ethernet frame it should impact src field + jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 11;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=13 + # provided L2 Ethernet simplest header is 14 bytes long, it should impact ethertype field + jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 13;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" +} +ethernet_in_mbuf_len_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "inet_in_mbuf_len" + atf_add_test_case "inet6_in_mbuf_len" + atf_add_test_case "ethernet_in_mbuf_len" +} diff --git a/tests/sys/netpfil/pf/modulate.sh b/tests/sys/netpfil/pf/modulate.sh index 98d5df14a44d..1abe22cff391 100644 --- a/tests/sys/netpfil/pf/modulate.sh +++ b/tests/sys/netpfil/pf/modulate.sh @@ -31,7 +31,7 @@ modulate_v4_head() { atf_set descr 'IPv4 TCP sequence number modulation' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } modulate_v4_body() @@ -54,7 +54,7 @@ modulate_v6_head() { atf_set descr 'IPv6 TCP sequence number modulation' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } modulate_v6_body() diff --git a/tests/sys/netpfil/pf/nat.sh b/tests/sys/netpfil/pf/nat.sh index 7cc0d8f35c96..16c981f97399 100644 --- a/tests/sys/netpfil/pf/nat.sh +++ b/tests/sys/netpfil/pf/nat.sh @@ -2,6 +2,8 @@ # SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2018 Kristof Provost <kp@FreeBSD.org> +# Copyright (c) 2025 Kajetan Staszkiewicz <ks@FreeBSD.org> +# Copyright (c) 2021 KUROSAWA Takahiro <takahiro.kurosawa@gmail.com> # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -51,7 +53,7 @@ exhaust_body() jexec nat sysctl net.inet.ip.forwarding=1 jexec echo ifconfig ${epair_echo}b 198.51.100.2/24 up - jexec echo /usr/sbin/inetd -p inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf + jexec echo /usr/sbin/inetd -p ${PWD}/inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf # Enable pf! jexec nat pfctl -e @@ -79,7 +81,6 @@ exhaust_body() exhaust_cleanup() { - rm -f inetd-echo.pid pft_cleanup } @@ -113,13 +114,717 @@ nested_anchor_body() } +endpoint_independent_setup() +{ + pft_init + filter="udp and dst port 1234" # only capture udp pings + + epair_client=$(vnet_mkepair) + epair_nat=$(vnet_mkepair) + epair_server1=$(vnet_mkepair) + epair_server2=$(vnet_mkepair) + bridge=$(vnet_mkbridge) + + vnet_mkjail nat ${epair_client}b ${epair_nat}a + vnet_mkjail client ${epair_client}a + vnet_mkjail server1 ${epair_server1}a + vnet_mkjail server2 ${epair_server2}a + + ifconfig ${epair_server1}b up + ifconfig ${epair_server2}b up + ifconfig ${epair_nat}b up + ifconfig ${bridge} \ + addm ${epair_server1}b \ + addm ${epair_server2}b \ + addm ${epair_nat}b \ + up + + jexec nat ifconfig ${epair_client}b 192.0.2.1/24 up + jexec nat ifconfig ${epair_nat}a 198.51.100.42/24 up + jexec nat sysctl net.inet.ip.forwarding=1 + + jexec client ifconfig ${epair_client}a 192.0.2.2/24 up + jexec client route add default 192.0.2.1 + + jexec server1 ifconfig ${epair_server1}a 198.51.100.32/24 up + jexec server2 ifconfig ${epair_server2}a 198.51.100.22/24 up +} + +endpoint_independent_common() +{ + # Enable pf! + jexec nat pfctl -e + + # validate non-endpoint independent nat rule behaviour + pft_set_rules nat "${1}" + + jexec server1 tcpdump -i ${epair_server1}a -w ${PWD}/server1.pcap \ + --immediate-mode $filter & + server1tcppid="$!" + jexec server2 tcpdump -i ${epair_server2}a -w ${PWD}/server2.pcap \ + --immediate-mode $filter & + server2tcppid="$!" + + # send out multiple packets + for i in $(seq 1 10); do + echo "ping" | jexec client nc -u 198.51.100.32 1234 -p 4242 -w 0 + echo "ping" | jexec client nc -u 198.51.100.22 1234 -p 4242 -w 0 + done + + kill $server1tcppid + kill $server2tcppid + + tuple_server1=$(tcpdump -r ${PWD}/server1.pcap | awk '{addr=$3} END {print addr}') + tuple_server2=$(tcpdump -r ${PWD}/server2.pcap | awk '{addr=$3} END {print addr}') + + if [ -z $tuple_server1 ] + then + atf_fail "server1 did not receive connection from client (default)" + fi + + if [ -z $tuple_server2 ] + then + atf_fail "server2 did not receive connection from client (default)" + fi + + if [ "$tuple_server1" = "$tuple_server2" ] + then + echo "server1 tcpdump: $tuple_server1" + echo "server2 tcpdump: $tuple_server2" + atf_fail "Received same IP:port on server1 and server2 (default)" + fi + + # validate endpoint independent nat rule behaviour + pft_set_rules nat "${2}" + + jexec server1 tcpdump -i ${epair_server1}a -w ${PWD}/server1.pcap \ + --immediate-mode $filter & + server1tcppid="$!" + jexec server2 tcpdump -i ${epair_server2}a -w ${PWD}/server2.pcap \ + --immediate-mode $filter & + server2tcppid="$!" + + # send out multiple packets, sometimes one fails to go through + for i in $(seq 1 10); do + echo "ping" | jexec client nc -u 198.51.100.32 1234 -p 4242 -w 0 + echo "ping" | jexec client nc -u 198.51.100.22 1234 -p 4242 -w 0 + done + + kill $server1tcppid + kill $server2tcppid + + tuple_server1=$(tcpdump -r ${PWD}/server1.pcap | awk '{addr=$3} END {print addr}') + tuple_server2=$(tcpdump -r ${PWD}/server2.pcap | awk '{addr=$3} END {print addr}') + + if [ -z $tuple_server1 ] + then + atf_fail "server1 did not receive connection from client (endpoint-independent)" + fi + + if [ -z $tuple_server2 ] + then + atf_fail "server2 did not receive connection from client (endpoint-independent)" + fi + + if [ ! "$tuple_server1" = "$tuple_server2" ] + then + echo "server1 tcpdump: $tuple_server1" + echo "server2 tcpdump: $tuple_server2" + atf_fail "Received different IP:port on server1 than server2 (endpoint-independent)" + fi +} + +atf_test_case "endpoint_independent_compat" "cleanup" +endpoint_independent_compat_head() +{ + atf_set descr 'Test that a client behind NAT gets the same external IP:port for different servers' + atf_set require.user root +} + +endpoint_independent_compat_body() +{ + endpoint_independent_setup # Sets ${epair_…} variables + + endpoint_independent_common \ + "nat on ${epair_nat}a inet from ! (${epair_nat}a) to any -> (${epair_nat}a)" \ + "nat on ${epair_nat}a inet from ! (${epair_nat}a) to any -> (${epair_nat}a) endpoint-independent" +} + +endpoint_independent_compat_cleanup() +{ + pft_cleanup + rm -f server1.out + rm -f server2.out +} + +atf_test_case "endpoint_independent_pass" "cleanup" +endpoint_independent_pass_head() +{ + atf_set descr 'Test that a client behind NAT gets the same external IP:port for different servers' + atf_set require.user root +} + +endpoint_independent_pass_body() +{ + endpoint_independent_setup # Sets ${epair_…} variables + + endpoint_independent_common \ + "pass out on ${epair_nat}a inet from ! (${epair_nat}a) to any nat-to (${epair_nat}a) keep state" \ + "pass out on ${epair_nat}a inet from ! (${epair_nat}a) to any nat-to (${epair_nat}a) endpoint-independent keep state" + +} + +endpoint_independent_pass_cleanup() +{ + pft_cleanup + rm -f server1.out + rm -f server2.out +} + nested_anchor_cleanup() { pft_cleanup } +atf_test_case "nat6_nolinklocal" "cleanup" +nat6_nolinklocal_head() +{ + atf_set descr 'Ensure we do not use link-local addresses' + atf_set require.user root +} + +nat6_nolinklocal_body() +{ + pft_init + + epair_nat=$(vnet_mkepair) + epair_echo=$(vnet_mkepair) + + vnet_mkjail nat ${epair_nat}b ${epair_echo}a + vnet_mkjail echo ${epair_echo}b + + ifconfig ${epair_nat}a inet6 2001:db8::2/64 no_dad up + route add -6 -net 2001:db8:1::/64 2001:db8::1 + + jexec nat ifconfig ${epair_nat}b inet6 2001:db8::1/64 no_dad up + jexec nat ifconfig ${epair_echo}a inet6 2001:db8:1::1/64 no_dad up + jexec nat sysctl net.inet6.ip6.forwarding=1 + + jexec echo ifconfig ${epair_echo}b inet6 2001:db8:1::2/64 no_dad up + # Ensure we can't reply to link-local pings + jexec echo pfctl -e + pft_set_rules echo \ + "pass" \ + "block in inet6 proto icmp6 from fe80::/10 to any icmp6-type echoreq" + + jexec nat pfctl -e + pft_set_rules nat \ + "nat pass on ${epair_echo}a inet6 from 2001:db8::/64 to any -> (${epair_echo}a)" \ + "pass" + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 2001:db8::1 + for i in `seq 0 10` + do + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 2001:db8:1::2 + done +} + +nat6_nolinklocal_cleanup() +{ + pft_cleanup +} + +empty_table_common() +{ + option=$1 + + pft_init + + epair_wan=$(vnet_mkepair) + epair_lan=$(vnet_mkepair) + + vnet_mkjail srv ${epair_wan}a + jexec srv ifconfig ${epair_wan}a 192.0.2.2/24 up + + vnet_mkjail rtr ${epair_wan}b ${epair_lan}a + jexec rtr ifconfig ${epair_wan}b 192.0.2.1/24 up + jexec rtr ifconfig ${epair_lan}a 198.51.100.1/24 up + jexec rtr sysctl net.inet.ip.forwarding=1 + + ifconfig ${epair_lan}b 198.51.100.2/24 up + route add default 198.51.100.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "table <empty>" \ + "nat on ${epair_wan}b inet from 198.51.100.0/24 -> <empty> ${option}" \ + "pass" + + # Sanity checks + atf_check -s exit:0 -o ignore \ + jexec rtr ping -c 1 192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping -c 1 198.51.100.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + # Provoke divide by zero + ping -c 1 192.0.2.2 + true +} + +atf_test_case "empty_table_source_hash" "cleanup" +empty_table_source_hash_head() +{ + atf_set descr 'Test source-hash on an emtpy table' + atf_set require.user root +} + +empty_table_source_hash_body() +{ + empty_table_common "source-hash" +} + +empty_table_source_hash_cleanup() +{ + pft_cleanup +} + +atf_test_case "empty_table_random" "cleanup" +empty_table_random_head() +{ + atf_set descr 'Test random on an emtpy table' + atf_set require.user root +} + +empty_table_random_body() +{ + empty_table_common "random" +} + +empty_table_random_cleanup() +{ + pft_cleanup +} + +no_addrs_common() +{ + option=$1 + + pft_init + + epair_wan=$(vnet_mkepair) + epair_lan=$(vnet_mkepair) + + vnet_mkjail srv ${epair_wan}a + jexec srv ifconfig ${epair_wan}a 192.0.2.2/24 up + + vnet_mkjail rtr ${epair_wan}b ${epair_lan}a + jexec rtr route add -net 192.0.2.0/24 -iface ${epair_wan}b + jexec rtr ifconfig ${epair_lan}a 198.51.100.1/24 up + jexec rtr sysctl net.inet.ip.forwarding=1 + + ifconfig ${epair_lan}b 198.51.100.2/24 up + route add default 198.51.100.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "nat on ${epair_wan}b inet from 198.51.100.0/24 -> (${epair_wan}b) ${option}" \ + "pass" + + # Provoke divide by zero + ping -c 1 192.0.2.2 + true +} + +atf_test_case "no_addrs_source_hash" "cleanup" +no_addrs_source_hash_head() +{ + atf_set descr 'Test source-hash on an interface with no addresses' + atf_set require.user root +} + +no_addrs_source_hash_body() +{ + no_addrs_common "source-hash" +} + +no_addrs_source_hash_cleanup() +{ + pft_cleanup +} + +atf_test_case "no_addrs_random" "cleanup" +no_addrs_random_head() +{ + atf_set descr 'Test random on an interface with no addresses' + atf_set require.user root +} + +no_addrs_random_body() +{ + no_addrs_common "random" +} + +no_addrs_random_cleanup() +{ + pft_cleanup +} + +nat_pass_head() +{ + atf_set descr 'IPv4 NAT on pass rule' + atf_set require.user root + atf_set require.progs scapy +} + +nat_pass_body() +{ + setup_router_server_ipv4 + # Delete the route back to make sure that the traffic has been NAT-ed + jexec server route del -net ${net_tester} ${net_server_host_router} + + pft_set_rules router \ + "block" \ + "pass in on ${epair_tester}b inet proto tcp keep state" \ + "pass out on ${epair_server}a inet proto tcp nat-to ${epair_server}a keep state" + + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 + + jexec router pfctl -qvvsr + jexec router pfctl -qvvss + jexec router ifconfig + jexec router netstat -rn +} + +nat_pass_cleanup() +{ + pft_cleanup +} + +nat_match_head() +{ + atf_set descr 'IPv4 NAT on match rule' + atf_set require.user root + atf_set require.progs scapy +} + +nat_match_body() +{ + setup_router_server_ipv4 + # Delete the route back to make sure that the traffic has been NAT-ed + jexec server route del -net ${net_tester} ${net_server_host_router} + + # NAT is applied during ruleset evaluation: + # rules after "match" match on NAT-ed address + pft_set_rules router \ + "block" \ + "pass in on ${epair_tester}b inet proto tcp keep state" \ + "match out on ${epair_server}a inet proto tcp nat-to ${epair_server}a" \ + "pass out on ${epair_server}a inet proto tcp from ${epair_server}a keep state" + + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 + + jexec router pfctl -qvvsr + jexec router pfctl -qvvss + jexec router ifconfig + jexec router netstat -rn +} + +nat_match_cleanup() +{ + pft_cleanup +} + +map_e_common() +{ + NC_TRY_COUNT=12 + + pft_init + + epair_map_e=$(vnet_mkepair) + epair_echo=$(vnet_mkepair) + + vnet_mkjail map_e ${epair_map_e}b ${epair_echo}a + vnet_mkjail echo ${epair_echo}b + + ifconfig ${epair_map_e}a 192.0.2.2/24 up + route add -net 198.51.100.0/24 192.0.2.1 + + jexec map_e ifconfig ${epair_map_e}b 192.0.2.1/24 up + jexec map_e ifconfig ${epair_echo}a 198.51.100.1/24 up + jexec map_e sysctl net.inet.ip.forwarding=1 + + jexec echo ifconfig ${epair_echo}b 198.51.100.2/24 up + jexec echo /usr/sbin/inetd -p ${PWD}/inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf + + # Enable pf! + jexec map_e pfctl -e +} + +atf_test_case "map_e_compat" "cleanup" +map_e_compat_head() +{ + atf_set descr 'map-e-portset test' + atf_set require.user root +} + +map_e_compat_body() +{ + map_e_common + + pft_set_rules map_e \ + "nat pass on ${epair_echo}a inet from 192.0.2.0/24 to any -> (${epair_echo}a) map-e-portset 2/12/0x342" + + # Only allow specified ports. + jexec echo pfctl -e + pft_set_rules echo "block return all" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 19720:19723 to (${epair_echo}b) port 7" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 36104:36107 to (${epair_echo}b) port 7" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 52488:52491 to (${epair_echo}b) port 7" \ + "set skip on lo" + + i=0 + while [ ${i} -lt ${NC_TRY_COUNT} ] + do + echo "foo ${i}" | timeout 2 nc -N 198.51.100.2 7 + if [ $? -ne 0 ]; then + atf_fail "nc failed (${i})" + fi + i=$((${i}+1)) + done +} + +map_e_compat_cleanup() +{ + pft_cleanup +} + + +atf_test_case "map_e_pass" "cleanup" +map_e_pass_head() +{ + atf_set descr 'map-e-portset test' + atf_set require.user root +} + +map_e_pass_body() +{ + map_e_common + + pft_set_rules map_e \ + "pass out on ${epair_echo}a inet from 192.0.2.0/24 to any nat-to (${epair_echo}a) map-e-portset 2/12/0x342 keep state" + + jexec map_e pfctl -qvvsr + + # Only allow specified ports. + jexec echo pfctl -e + pft_set_rules echo "block return all" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 19720:19723 to (${epair_echo}b) port 7" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 36104:36107 to (${epair_echo}b) port 7" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 52488:52491 to (${epair_echo}b) port 7" \ + "set skip on lo" + + i=0 + while [ ${i} -lt ${NC_TRY_COUNT} ] + do + echo "foo ${i}" | timeout 2 nc -N 198.51.100.2 7 + if [ $? -ne 0 ]; then + atf_fail "nc failed (${i})" + fi + i=$((${i}+1)) + done +} + +map_e_pass_cleanup() +{ + pft_cleanup +} + +binat_compat_head() +{ + atf_set descr 'IPv4 BINAT with nat ruleset' + atf_set require.user root + atf_set require.progs scapy +} + +binat_compat_body() +{ + setup_router_server_ipv4 + # Delete the route back to make sure that the traffic has been NAT-ed + jexec server route del -net ${net_tester} ${net_server_host_router} + + pft_set_rules router \ + "set state-policy if-bound" \ + "set ruleset-optimization none" \ + "binat on ${epair_server}a inet proto tcp from ${net_tester_host_tester} to any tag sometag -> ${epair_server}a" \ + "block" \ + "pass in on ${epair_tester}b inet proto tcp !tagged sometag keep state" \ + "pass out on ${epair_server}a inet proto tcp tagged sometag keep state" \ + "pass in on ${epair_server}a inet proto tcp tagged sometag keep state" \ + "pass out on ${epair_tester}b inet proto tcp tagged sometag keep state" + + # Test the outbound NAT part of BINAT. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + + for state_regexp in \ + "${epair_tester}b tcp ${net_server_host_server}:9 <- ${net_tester_host_tester}:4201 .* 3:2 pkts,.* rule 1" \ + "${epair_server}a tcp ${net_server_host_router}:4201 \(${net_tester_host_tester}:4201\) -> ${net_server_host_server}:9 .* 3:2 pkts,.* rule 2" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + + # Test the inbound RDR part of BINAT. + # The "tester" becomes "server" and vice versa. + inetd_conf=$(mktemp) + echo "discard stream tcp nowait root internal" > $inetd_conf + inetd -p ${PWD}/inetd_tester.pid $inetd_conf + + atf_check -s exit:0 \ + jexec server ${common_dir}/pft_ping.py \ + --ping-type=tcp3way --send-sport=4202 \ + --sendif ${epair_server}b \ + --to ${net_server_host_router} \ + --replyif ${epair_server}b + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + + for state_regexp in \ + "${epair_server}a tcp ${net_tester_host_tester}:9 \(${net_server_host_router}:9\) <- ${net_server_host_server}:4202 .* 3:2 pkts,.* rule 3" \ + "${epair_tester}b tcp ${net_server_host_server}:4202 -> ${net_tester_host_tester}:9 .* 3:2 pkts,.* rule 4" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done +} + +binat_compat_cleanup() +{ + pft_cleanup + kill $(cat ${PWD}/inetd_tester.pid) +} + +binat_match_head() +{ + atf_set descr 'IPv4 BINAT with nat ruleset' + atf_set require.user root + atf_set require.progs scapy +} + +binat_match_body() +{ + setup_router_server_ipv4 + # Delete the route back to make sure that the traffic has been NAT-ed + jexec server route del -net ${net_tester} ${net_server_host_router} + + # The "binat-to" rule expands to 2 rules so the ""pass" rules start at 3! + pft_set_rules router \ + "set state-policy if-bound" \ + "set ruleset-optimization none" \ + "block" \ + "match on ${epair_server}a inet proto tcp from ${net_tester_host_tester} to any tag sometag binat-to ${epair_server}a" \ + "pass in on ${epair_tester}b inet proto tcp !tagged sometag keep state" \ + "pass out on ${epair_server}a inet proto tcp tagged sometag keep state" \ + "pass in on ${epair_server}a inet proto tcp tagged sometag keep state" \ + "pass out on ${epair_tester}b inet proto tcp tagged sometag keep state" + + # Test the outbound NAT part of BINAT. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + + for state_regexp in \ + "${epair_tester}b tcp ${net_server_host_server}:9 <- ${net_tester_host_tester}:4201 .* 3:2 pkts,.* rule 3" \ + "${epair_server}a tcp ${net_server_host_router}:4201 \(${net_tester_host_tester}:4201\) -> ${net_server_host_server}:9 .* 3:2 pkts,.* rule 4" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + + # Test the inbound RDR part of BINAT. + # The "tester" becomes "server" and vice versa. + inetd_conf=$(mktemp) + echo "discard stream tcp nowait root internal" > $inetd_conf + inetd -p ${PWD}/inetd_tester.pid $inetd_conf + + atf_check -s exit:0 \ + jexec server ${common_dir}/pft_ping.py \ + --ping-type=tcp3way --send-sport=4202 \ + --sendif ${epair_server}b \ + --to ${net_server_host_router} \ + --replyif ${epair_server}b + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + + for state_regexp in \ + "${epair_server}a tcp ${net_tester_host_tester}:9 \(${net_server_host_router}:9\) <- ${net_server_host_server}:4202 .* 3:2 pkts,.* rule 5" \ + "${epair_tester}b tcp ${net_server_host_server}:4202 -> ${net_tester_host_tester}:9 .* 3:2 pkts,.* rule 6" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done +} + +binat_match_cleanup() +{ + pft_cleanup + kill $(cat ${PWD}/inetd_tester.pid) +} + +atf_test_case "empty_pool" "cleanup" +empty_pool_head() +{ + atf_set descr 'NAT with empty pool' + atf_set require.user root +} + +empty_pool_body() +{ + pft_init + setup_router_server_ipv6 + + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b" \ + "pass out on ${epair_server}a inet6 from any to ${net_server_host_server} nat-to <nonexistent>" \ + + # pf_map_addr_sn() won't be able to pick a target address, because + # the table used in redireciton pool is empty. Packet will not be + # forwarded, error counter will be increased. + ping_server_check_reply exit:1 + # Ignore warnings about not-loaded ALTQ + atf_check -o "match:map-failed +1 +" -x "jexec router pfctl -qvvsi 2> /dev/null" +} + +empty_pool_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "exhaust" atf_add_test_case "nested_anchor" + atf_add_test_case "endpoint_independent_compat" + atf_add_test_case "endpoint_independent_pass" + atf_add_test_case "nat6_nolinklocal" + atf_add_test_case "empty_table_source_hash" + atf_add_test_case "no_addrs_source_hash" + atf_add_test_case "empty_table_random" + atf_add_test_case "no_addrs_random" + atf_add_test_case "map_e_compat" + atf_add_test_case "map_e_pass" + atf_add_test_case "nat_pass" + atf_add_test_case "nat_match" + atf_add_test_case "binat_compat" + atf_add_test_case "binat_match" + atf_add_test_case "empty_pool" } diff --git a/tests/sys/netpfil/pf/nat64.py b/tests/sys/netpfil/pf/nat64.py new file mode 100644 index 000000000000..adae2489ce5e --- /dev/null +++ b/tests/sys/netpfil/pf/nat64.py @@ -0,0 +1,274 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Rubicon Communications, LLC (Netgate) +# +# 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. + +import pytest +import selectors +import socket +import sys +from utils import DelayedSend +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +class TestNAT64(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2", "if3"]}, + "vnet4": {"ifaces": ["if3"]}, + "if1": {"prefixes6": [("2001:db8::2/64", "2001:db8::1/64")]}, + "if2": {"prefixes4": [("192.0.2.1/24", "192.0.2.2/24")]}, + "if3": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]} + } + + def vnet4_handler(self, vnet): + ToolsHelper.print_output("/sbin/route add default 198.51.100.1") + + def vnet3_handler(self, vnet): + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.ttl=62") + ToolsHelper.print_output("/sbin/sysctl net.inet.udp.checksum=0") + + sel = selectors.DefaultSelector() + t = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + t.bind(("0.0.0.0", 1234)) + t.setblocking(False) + t.listen() + sel.register(t, selectors.EVENT_READ, data=None) + + u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + u.bind(("0.0.0.0", 4444)) + u.setblocking(False) + sel.register(u, selectors.EVENT_READ, data="UDP") + + while True: + events = sel.select(timeout=20) + for key, mask in events: + sock = key.fileobj + if key.data is None: + conn, addr = sock.accept() + print(f"Accepted connection from {addr}") + data = types.SimpleNamespace(addr=addr, inb=b"", outb=b"") + events = selectors.EVENT_READ | selectors.EVENT_WRITE + sel.register(conn, events, data=data) + elif key.data == "UDP": + recv_data, addr = sock.recvfrom(1024) + print(f"Received UDP {recv_data} from {addr}") + sock.sendto(b"foo", addr) + else: + if mask & selectors.EVENT_READ: + recv_data = sock.recv(1024) + print(f"Received TCP {recv_data}") + sock.send(b"foo") + else: + print("Unknown event?") + t.close() + u.close() + return + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + + ToolsHelper.print_output("/sbin/route add default 192.0.2.2") + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "pass inet6 proto icmp6", + "pass in on %s inet6 af-to inet from 192.0.2.1" % ifname]) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_tcp_rst(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + import scapy.all as sp + + # Send a SYN + packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \ + / sp.TCP(dport=1222, flags="S") + + # Get a reply + reply = sp.sr1(packet) + + # We expect to get a RST here. + tcp = reply.getlayer(sp.TCP) + assert tcp + assert "R" in tcp.flags + + # Now try to SYN to an open port + packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \ + / sp.TCP(dport=1234, flags="S") + reply = sp.sr1(packet) + + tcp = reply.getlayer(sp.TCP) + assert tcp + + # We don't get RST + assert "R" not in tcp.flags + + # We do get SYN|ACK + assert "S" in tcp.flags + assert "A" in tcp.flags + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_udp_port_closed(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \ + / sp.UDP(dport=1222) / sp.Raw("bar") + reply = sp.sr1(packet, timeout=3) + print(reply.show()) + + # We expect an ICMPv6 error, not a UDP reply + assert not reply.getlayer(sp.UDP) + icmp = reply.getlayer(sp.ICMPv6DestUnreach) + assert icmp + assert icmp.type == 1 + assert icmp.code == 4 + udp = reply.getlayer(sp.UDPerror) + assert udp + assert udp.dport == 1222 + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_address_unreachable(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::203.0.113.2") \ + / sp.UDP(dport=1222) / sp.Raw("bar") + reply = sp.sr1(packet, timeout=3) + print(reply.show()) + + # We expect an ICMPv6 error, not a UDP reply + assert not reply.getlayer(sp.UDP) + icmp = reply.getlayer(sp.ICMPv6DestUnreach) + assert icmp + assert icmp.type == 1 + assert icmp.code == 0 + udp = reply.getlayer(sp.UDPerror) + assert udp + assert udp.dport == 1222 + + # Check the hop limit + ip6 = reply.getlayer(sp.IPv6) + assert ip6.hlim == 61 + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_udp_checksum(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + import scapy.all as sp + + # Send an outbound UDP packet to establish state + packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \ + / sp.UDP(sport=3333, dport=4444) / sp.Raw("foo") + + # Get a reply + # We'll send the reply without UDP checksum on the IPv4 side + # but that's not valid for IPv6, so expect pf to update the checksum. + reply = sp.sr1(packet, timeout=5) + + udp = reply.getlayer(sp.UDP) + assert udp + assert udp.chksum != 0 + + def common_test_source_addr(self, packet): + vnet = self.vnet_map["vnet1"] + sendif = vnet.iface_alias_map["if1"].name + + import scapy.all as sp + + print("Outbound:\n") + packet.show() + + s = DelayedSend(packet) + + # We expect an ICMPv6 error here, where we'll verify the source address of + # the outer packet + packets = sp.sniff(iface=sendif, timeout=5) + + for reply in packets: + print("Reply:\n") + reply.show() + icmp = reply.getlayer(sp.ICMPv6TimeExceeded) + if not icmp: + continue + + ip = reply.getlayer(sp.IPv6) + assert icmp + assert ip.src == "64:ff9b::c000:202" + return reply + + # If we don't find the packet we expect to see + assert False + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_source_addr_tcp(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \ + / sp.TCP(sport=1111, dport=2222, flags="S") + self.common_test_source_addr(packet) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_source_addr_udp(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \ + / sp.UDP(sport=1111, dport=2222) / sp.Raw("foo") + self.common_test_source_addr(packet) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_source_addr_sctp(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \ + / sp.SCTP(sport=1111, dport=2222) \ + / sp.SCTPChunkInit(init_tag=1, n_in_streams=1, n_out_streams=1, a_rwnd=1500) + self.common_test_source_addr(packet) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_source_addr_icmp(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \ + / sp.ICMPv6EchoRequest() / sp.Raw("foo") + reply = self.common_test_source_addr(packet) + icmp = reply.getlayer(sp.ICMPv6EchoRequest) + assert icmp diff --git a/tests/sys/netpfil/pf/nat64.sh b/tests/sys/netpfil/pf/nat64.sh new file mode 100644 index 000000000000..0bba1470c4c5 --- /dev/null +++ b/tests/sys/netpfil/pf/nat64.sh @@ -0,0 +1,1056 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Rubicon Communications, LLC (Netgate) +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +nat64_setup_base() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.1 + + jexec rtr pfctl -e +} + +nat64_setup_in() +{ + nat64_setup_base + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" +} + +nat64_setup_out() +{ + nat64_setup_base + jexec rtr sysctl net.inet6.ip6.forwarding=1 + # AF translation happens post-routing, traffic must be directed + # towards the outbound interface using routes for the original AF. + # jexec rtr ifconfig ${epair_link}a inet6 2001:db8:2::1/64 up no_dad + jexec rtr route add -inet6 64:ff9b::/96 -iface ${epair_link}a; + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass quick inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in quick on ${epair}b from any to 64:ff9b::/96" \ + "pass out quick on ${epair_link}a from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" \ + "block" +} + +atf_test_case "icmp_echo_in" "cleanup" +icmp_echo_in_head() +{ + atf_set descr 'Basic NAT64 ICMP echo test on inbound interface' + atf_set require.user root +} + +icmp_echo_in_body() +{ + nat64_setup_in + + # One ping + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + # Make sure packets make it even when state is established + atf_check -s exit:0 \ + -o match:'5 packets transmitted, 5 packets received, 0.0% packet loss' \ + ping6 -c 5 64:ff9b::192.0.2.2 +} + +icmp_echo_in_cleanup() +{ + pft_cleanup +} + +atf_test_case "icmp_echo_out" "cleanup" +icmp_echo_out_head() +{ + atf_set descr 'Basic NAT64 ICMP echo test on outbound interface' + atf_set require.user root +} + +icmp_echo_out_body() +{ + nat64_setup_out + + # One ping + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + # Make sure packets make it even when state is established + atf_check -s exit:0 \ + -o match:'5 packets transmitted, 5 packets received, 0.0% packet loss' \ + ping6 -c 5 64:ff9b::192.0.2.2 +} + +icmp_echo_out_cleanup() +{ + pft_cleanup +} + +atf_test_case "fragmentation_in" "cleanup" +fragmentation_in_head() +{ + atf_set descr 'Test fragmented packets on inbound interface' + atf_set require.user root +} + +fragmentation_in_body() +{ + nat64_setup_in + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 -s 1280 64:ff9b::192.0.2.2 + + atf_check -s exit:0 \ + -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \ + ping6 -c 3 -s 2000 64:ff9b::192.0.2.2 + atf_check -s exit:0 \ + -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \ + ping6 -c 3 -s 10000 -b 20000 64:ff9b::192.0.2.2 +} + +fragmentation_in_cleanup() +{ + pft_cleanup +} + +atf_test_case "fragmentation_out" "cleanup" +fragmentation_out_head() +{ + atf_set descr 'Test fragmented packets on outbound interface' + atf_set require.user root +} + +fragmentation_out_body() +{ + nat64_setup_out + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 -s 1280 64:ff9b::192.0.2.2 + + atf_check -s exit:0 \ + -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \ + ping6 -c 3 -s 2000 64:ff9b::192.0.2.2 + atf_check -s exit:0 \ + -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \ + ping6 -c 3 -s 10000 -b 20000 64:ff9b::192.0.2.2 +} + +fragmentation_out_cleanup() +{ + pft_cleanup +} + +atf_test_case "tcp_in" "cleanup" +tcp_in_head() +{ + atf_set descr 'TCP NAT64 test on inbound interface' + atf_set require.user root +} + +tcp_in_body() +{ + nat64_setup_in + + echo "foo" | jexec dst nc -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(nc -w 3 -6 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to TCP server" + fi +} + +tcp_in_cleanup() +{ + pft_cleanup +} + +atf_test_case "tcp_out" "cleanup" +tcp_out_head() +{ + atf_set descr 'TCP NAT64 test on outbound interface' + atf_set require.user root +} + +tcp_out_body() +{ + nat64_setup_out + + echo "foo" | jexec dst nc -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(nc -w 3 -6 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to TCP server" + fi +} + +tcp_out_cleanup() +{ + pft_cleanup +} + +atf_test_case "udp_in" "cleanup" +udp_in_head() +{ + atf_set descr 'UDP NAT64 test on inbound interface' + atf_set require.user root +} + +udp_in_body() +{ + nat64_setup_in + + echo "foo" | jexec dst nc -u -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(echo bar | nc -w 3 -6 -u 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to UDP server" + fi +} + +udp_in_cleanup() +{ + pft_cleanup +} + +atf_test_case "udp_out" "cleanup" +udp_out_head() +{ + atf_set descr 'UDP NAT64 test on outbound interface' + atf_set require.user root +} + +udp_out_body() +{ + nat64_setup_out + + echo "foo" | jexec dst nc -u -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(echo bar | nc -w 3 -6 -u 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to UDP server" + fi +} + +udp_out_cleanup() +{ + pft_cleanup +} + +atf_test_case "sctp_in" "cleanup" +sctp_in_head() +{ + atf_set descr 'SCTP NAT64 test on inbound interface' + atf_set require.user root +} + +sctp_in_body() +{ + nat64_setup_in + if ! kldstat -q -m sctp; then + atf_skip "This test requires SCTP" + fi + + echo "foo" | jexec dst nc --sctp -N -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(echo bar | nc --sctp -w 3 -6 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to SCTP server" + fi +} + +sctp_in_cleanup() +{ + pft_cleanup +} + +atf_test_case "sctp_out" "cleanup" +sctp_out_head() +{ + atf_set descr 'SCTP NAT64 test on outbound interface' + atf_set require.user root +} + +sctp_out_body() +{ + nat64_setup_out + if ! kldstat -q -m sctp; then + atf_skip "This test requires SCTP" + fi + + echo "foo" | jexec dst nc --sctp -N -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(echo bar | nc --sctp -w 3 -6 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to SCTP server" + fi +} + +sctp_out_cleanup() +{ + pft_cleanup +} + +atf_test_case "tos" "cleanup" +tos_head() +{ + atf_set descr 'ToS translation test' + atf_set require.user root +} + +tos_body() +{ + nat64_setup_in + + # Ensure we can distinguish ToS on the destination + jexec dst pfctl -e + pft_set_rules dst \ + "pass" \ + "block in inet tos 8" + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 -z 4 64:ff9b::192.0.2.2 + atf_check -s exit:2 -o ignore \ + ping6 -c 1 -z 8 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 -z 16 64:ff9b::192.0.2.2 + + jexec dst pfctl -sr -vv +} + +tos_cleanup() +{ + pft_cleanup +} + +atf_test_case "no_v4" "cleanup" +no_v4_head() +{ + atf_set descr 'Test error handling when there is no IPv4 address to translate to' + atf_set require.user root +} + +no_v4_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" + + atf_check -s exit:2 -o ignore \ + ping6 -c 3 64:ff9b::192.0.2.2 +} + +no_v4_cleanup() +{ + pft_cleanup +} + +atf_test_case "range" "cleanup" +range_head() +{ + atf_set descr 'Test using an address range for the IPv4 side' + atf_set require.user root +} + +range_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.2/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.254/24 up + jexec dst route add default 192.0.2.2 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + jexec rtr ping -c 1 192.0.2.254 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.2 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.3 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from 192.0.2.2/31 round-robin" + + # Use pf to count sources + jexec dst pfctl -e + pft_set_rules dst \ + "pass" + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.254 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.254 + + # Verify on dst that we saw different source addresses + atf_check -s exit:0 -o match:".*192.0.2.2.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.3.*" \ + jexec dst pfctl -ss +} + +range_cleanup() +{ + pft_cleanup +} + +atf_test_case "pool" "cleanup" +pool_head() +{ + atf_set descr 'Use a pool of IPv4 addresses' + atf_set require.user root +} + +pool_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.4/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from { 192.0.2.1, 192.0.2.3, 192.0.2.4 } round-robin" + + # Use pf to count sources + jexec dst pfctl -e + pft_set_rules dst \ + "pass" + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + # Verify on dst that we saw different source addresses + atf_check -s exit:0 -o match:".*192.0.2.1.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.3.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.4.*" \ + jexec dst pfctl -ss +} + +pool_cleanup() +{ + pft_cleanup +} + + +atf_test_case "table" +table_head() +{ + atf_set descr 'Check table restrictions' + atf_set require.user root +} + +table_body() +{ + pft_init + + # Round-robin and random are allowed + echo "pass in on epair inet6 from any to 64:ff9b::/96 af-to inet from <wanaddr> round-robin" | \ + atf_check -s exit:0 \ + pfctl -f - + echo "pass in on epair inet6 from any to 64:ff9b::/96 af-to inet from <wanaddr> random" | \ + atf_check -s exit:0 \ + pfctl -f - + + # bitmask is not + echo "pass in on epair inet6 from any to 64:ff9b::/96 af-to inet from <wanaddr> bitmask" | \ + atf_check -s exit:1 \ + -e match:"tables are not supported by pool type" \ + pfctl -f - +} + +table_cleanup() +{ + pft_cleanup +} + +atf_test_case "table_range" "cleanup" +table_range_head() +{ + atf_set descr 'Test using an address range within a table for the IPv4 side' + atf_set require.user root +} + +table_range_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.2/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.254/24 up + jexec dst route add default 192.0.2.2 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.2 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "table <wanaddrs> { 192.0.2.2/31 }" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from <wanaddrs> round-robin" + + # Use pf to count sources + jexec dst pfctl -e + pft_set_rules dst \ + "pass" + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.254 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.254 + + # Verify on dst that we saw different source addresses + atf_check -s exit:0 -o match:".*192.0.2.2.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.3.*" \ + jexec dst pfctl -ss +} + +table_range_cleanup() +{ + pft_cleanup +} + +table_common_body() +{ + pool_type=$1 + + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.4/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "table <wanaddrs> { 192.0.2.1, 192.0.2.3, 192.0.2.4 }" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from <wanaddrs> ${pool_type}" + + # Use pf to count sources + jexec dst pfctl -e + pft_set_rules dst \ + "pass" + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + # XXX We can't reasonably check pool type random because it's random. It may end + # up choosing the same source IP for all three connections. + if [ "${pool_type}" == "round-robin" ]; + then + # Verify on dst that we saw different source addresses + atf_check -s exit:0 -o match:".*192.0.2.1.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.3.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.4.*" \ + jexec dst pfctl -ss + fi +} + +atf_test_case "table_round_robin" "cleanup" +table_round_robin_head() +{ + atf_set descr 'Use a table of IPv4 addresses in round-robin mode' + atf_set require.user root +} + +table_round_robin_body() +{ + table_common_body round-robin +} + +table_round_robin_cleanup() +{ + pft_cleanup +} + +atf_test_case "table_random" "cleanup" +table_random_head() +{ + atf_set descr 'Use a table of IPv4 addresses in random mode' + atf_set require.user root +} + +table_random_body() +{ + table_common_body random +} + +table_random_cleanup() +{ + pft_cleanup +} + +atf_test_case "dummynet" "cleanup" +dummynet_head() +{ + atf_set descr 'Test dummynet on af-to rules' + atf_set require.user root +} + +dummynet_body() +{ + pft_init + dummynet_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.1 + + jexec rtr pfctl -e + jexec rtr dnctl pipe 1 config delay 600 + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 dnpipe 1 af-to inet from (${epair_link}a)" + + # The ping request will pass, but take 1.2 seconds (.6 in, .6 out) + # So this works: + atf_check -s exit:0 -o ignore \ + ping6 -c 1 -t 2 64:ff9b::192.0.2.2 + + # But this times out: + atf_check -s exit:2 -o ignore \ + ping6 -c 1 -t 1 64:ff9b::192.0.2.2 +} + +dummynet_cleanup() +{ + pft_cleanup +} + +atf_test_case "gateway6" "cleanup" +gateway6_head() +{ + atf_set descr 'NAT64 with a routing hop on the v6 side' + atf_set require.user root +} + +gateway6_body() +{ + pft_init + + epair_lan_link=$(vnet_mkepair) + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8:1::2/64 up no_dad + route -6 add default 2001:db8:1::1 + + vnet_mkjail lan_rtr ${epair}b ${epair_lan_link}a + jexec lan_rtr ifconfig ${epair}b inet6 2001:db8:1::1/64 up no_dad + jexec lan_rtr ifconfig ${epair_lan_link}a inet6 2001:db8::2/64 up no_dad + jexec lan_rtr route -6 add default 2001:db8::1 + jexec lan_rtr sysctl net.inet6.ip6.forwarding=1 + + vnet_mkjail rtr ${epair_lan_link}b ${epair_link}a + jexec rtr ifconfig ${epair_lan_link}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + jexec rtr route -6 add default 2001:db8::2 + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8:1::1 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair_lan_link}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" + + # One ping + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + # Make sure packets make it even when state is established + atf_check -s exit:0 \ + -o match:'5 packets transmitted, 5 packets received, 0.0% packet loss' \ + ping6 -c 5 64:ff9b::192.0.2.2 +} + +gateway6_cleanup() +{ + pft_cleanup +} + +atf_test_case "route_to" "cleanup" +route_to_head() +{ + atf_set descr 'Test route-to on af-to rules' + atf_set require.user root +} + +route_to_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair_null=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a ${epair_null}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_null}a 192.0.2.3/24 up + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b route-to (${epair_link}a 192.0.2.2) inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" + + atf_check -s exit:0 -o ignore \ + ping6 -c 3 64:ff9b::192.0.2.2 +} + +route_to_cleanup() +{ + pft_cleanup +} + +atf_test_case "reply_to" "cleanup" +reply_to_head() +{ + atf_set descr 'Test reply-to on af-to rules' + atf_set require.user root +} + +reply_to_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b reply-to (${epair}b 2001:db8::2) inet6 from any to 64:ff9b::/96 af-to inet from 192.0.2.1" + + atf_check -s exit:0 -o ignore \ + ping6 -c 3 64:ff9b::192.0.2.2 +} + +reply_to_cleanup() +{ + pft_cleanup +} + +atf_test_case "v6_gateway" "cleanup" +v6_gateway_head() +{ + atf_set descr 'nat64 when the IPv4 gateway is given by an IPv6 address' + atf_set require.user root +} + +v6_gateway_body() +{ + pft_init + + epair_wan_two=$(vnet_mkepair) + epair_wan_one=$(vnet_mkepair) + epair_lan=$(vnet_mkepair) + + ifconfig ${epair_lan}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair_lan}b ${epair_wan_one}a + jexec rtr ifconfig ${epair_lan}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_wan_one}a 192.0.2.1/24 up + jexec rtr ifconfig ${epair_wan_one}a inet6 -ifdisabled + jexec rtr route add default -inet6 fe80::1%${epair_wan_one}a + #jexec rtr route add default 192.0.2.2 + + vnet_mkjail wan_one ${epair_wan_one}b ${epair_wan_two}a + jexec wan_one ifconfig ${epair_wan_one}b 192.0.2.2/24 up + jexec wan_one ifconfig ${epair_wan_one}b inet6 fe80::1/64 + jexec wan_one ifconfig ${epair_wan_two}a 198.51.100.2/24 up + jexec wan_one route add default 192.0.2.1 + jexec wan_one sysctl net.inet.ip.forwarding=1 + + vnet_mkjail wan_two ${epair_wan_two}b + jexec wan_two ifconfig ${epair_wan_two}b 198.51.100.1/24 up + jexec wan_two route add default 198.51.100.2 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec rtr ping -c 1 192.0.2.2 + atf_check -s exit:0 -o ignore \ + jexec rtr ping -c 1 198.51.100.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair_lan}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_wan_one}a)" + + atf_check -s exit:0 -o ignore \ + ping6 -c 3 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 3 64:ff9b::198.51.100.1 +} + +v6_gateway_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "icmp_echo_in" + atf_add_test_case "icmp_echo_out" + atf_add_test_case "fragmentation_in" + atf_add_test_case "fragmentation_out" + atf_add_test_case "tcp_in" + atf_add_test_case "tcp_out" + atf_add_test_case "udp_in" + atf_add_test_case "udp_out" + atf_add_test_case "sctp_in" + atf_add_test_case "sctp_out" + atf_add_test_case "tos" + atf_add_test_case "no_v4" + atf_add_test_case "range" + atf_add_test_case "pool" + atf_add_test_case "table" + atf_add_test_case "table_range" + atf_add_test_case "table_round_robin" + atf_add_test_case "table_random" + atf_add_test_case "dummynet" + atf_add_test_case "gateway6" + atf_add_test_case "route_to" + atf_add_test_case "reply_to" + atf_add_test_case "v6_gateway" +} diff --git a/tests/sys/netpfil/pf/nat66.py b/tests/sys/netpfil/pf/nat66.py index 3a037ac710fc..16b4ef3dd02b 100644 --- a/tests/sys/netpfil/pf/nat66.py +++ b/tests/sys/netpfil/pf/nat66.py @@ -29,23 +29,10 @@ import ipaddress import pytest import re import socket -import threading -import time +from utils import DelayedSend from atf_python.sys.net.tools import ToolsHelper from atf_python.sys.net.vnet import VnetTestTemplate -class DelayedSend(threading.Thread): - def __init__(self, packet): - threading.Thread.__init__(self) - self._packet = packet - - self.start() - - def run(self): - import scapy.all as sp - time.sleep(1) - sp.send(self._packet) - class TestNAT66(VnetTestTemplate): REQUIRED_MODULES = [ "pf" ] TOPOLOGY = { @@ -140,6 +127,7 @@ class TestNAT66(VnetTestTemplate): assert found @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) def test_npt_icmp(self): cl_vnet = self.vnet_map["vnet1"] ifname = cl_vnet.iface_alias_map["if1"].name @@ -168,6 +156,7 @@ class TestNAT66(VnetTestTemplate): self.check_icmp_too_big(sp, 12000, 5000) @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) def test_npt_route_to_icmp(self): cl_vnet = self.vnet_map["vnet1"] ifname = cl_vnet.iface_alias_map["if1"].name diff --git a/tests/sys/netpfil/pf/pass_block.sh b/tests/sys/netpfil/pf/pass_block.sh index faf5c2de4de2..e955068d014b 100644 --- a/tests/sys/netpfil/pf/pass_block.sh +++ b/tests/sys/netpfil/pf/pass_block.sh @@ -232,7 +232,7 @@ urpf_head() { atf_set descr "Test unicast reverse path forwarding check" atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } urpf_body() @@ -292,6 +292,165 @@ urpf_cleanup() pft_cleanup } +atf_test_case "received_on" "cleanup" +received_on_head() +{ + atf_set descr 'Test received-on filtering' + atf_set require.user root +} + +received_on_body() +{ + pft_init + + epair_one=$(vnet_mkepair) + epair_two=$(vnet_mkepair) + epair_route=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair_one}b ${epair_two}b ${epair_route}a + vnet_mkjail srv ${epair_route}b + + ifconfig ${epair_one}a 192.0.2.2/24 up + ifconfig ${epair_two}a 198.51.100.2/24 up + route add 203.0.113.2 192.0.2.1 + route add 203.0.113.3 198.51.100.1 + + jexec alcatraz ifconfig ${epair_one}b 192.0.2.1/24 up + jexec alcatraz ifconfig ${epair_two}b 198.51.100.1/24 up + jexec alcatraz ifconfig ${epair_route}a 203.0.113.1/24 up + jexec alcatraz sysctl net.inet.ip.forwarding=1 + + jexec srv ifconfig ${epair_route}b 203.0.113.2/24 up + jexec srv ifconfig ${epair_route}b inet alias 203.0.113.3/24 up + jexec srv route add default 203.0.113.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 198.51.100.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.2 + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.3 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block in" \ + "pass received-on ${epair_one}b" + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 1 198.51.100.1 + + # And ensure we can check the received-on interface after routing + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.2 + atf_check -s exit:2 -o ignore \ + ping -c 1 203.0.113.3 + + # Now try this with a group instead + jexec alcatraz ifconfig ${epair_one}b group test + pft_set_rules alcatraz \ + "block in" \ + "pass received-on test" + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 1 198.51.100.1 + + # And ensure we can check the received-on interface after routing + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.2 + atf_check -s exit:2 -o ignore \ + ping -c 1 203.0.113.3 + + # Test '! received-on' + pft_set_rules alcatraz \ + "pass in" \ + "block ! received-on ${epair_one}b" + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 1 198.51.100.1 +} + +received_on_cleanup() +{ + pft_cleanup +} + +atf_test_case "optimize_any" "cleanup" +optimize_any_head() +{ + atf_set descr 'Test known optimizer bug' + atf_set require.user root +} + +optimize_any_body() +{ + pft_init + + epair=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" \ + "pass in inet from { any, 192.0.2.3 }" + + atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1 +} + +optimize_any_cleanup() +{ + pft_cleanup +} + +atf_test_case "any_if" "cleanup" +any_if_head() +{ + atf_set descr 'Test the any interface keyword' + atf_set require.user root +} + +any_if_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" \ + "pass in on any" + + atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1 +} + +any_if_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "enable_disable" @@ -300,4 +459,7 @@ atf_init_test_cases() atf_add_test_case "noalias" atf_add_test_case "nested_inline" atf_add_test_case "urpf" + atf_add_test_case "received_on" + atf_add_test_case "optimize_any" + atf_add_test_case "any_if" } diff --git a/tests/sys/netpfil/pf/pflog.sh b/tests/sys/netpfil/pf/pflog.sh index 75b7c5c217bb..a34ec893a75c 100644 --- a/tests/sys/netpfil/pf/pflog.sh +++ b/tests/sys/netpfil/pf/pflog.sh @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2023 Rubicon Communications, LLC (Netgate) +# Copyright (c) 2024 Deciso B.V. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -33,7 +34,7 @@ malformed_head() { atf_set descr 'Test that we do not log malformed packets as passing' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } malformed_body() @@ -79,7 +80,327 @@ malformed_cleanup() pft_cleanup } +atf_test_case "matches" "cleanup" +matches_head() +{ + atf_set descr 'Test the pflog matches keyword' + atf_set require.user root +} + +matches_body() +{ + pflog_init + + epair=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + ifconfig ${epair}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + jexec alcatraz ifconfig pflog0 up + pft_set_rules alcatraz \ + "match log(matches) inet proto icmp" \ + "match log(matches) inet from 192.0.2.2" \ + "pass" + + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog.txt & + sleep 1 # Wait for tcpdump to start + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + echo "Rules" + jexec alcatraz pfctl -sr -vv + echo "States" + jexec alcatraz pfctl -ss -vv + echo "Log" + cat ${PWD}/pflog.txt + + atf_check -o match:".*rule 0/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog.txt + atf_check -o match:".*rule 1/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog.txt +} + +matches_cleanup() +{ + pft_cleanup +} + +atf_test_case "matches_logif" "cleanup" +matches_logif_head() +{ + atf_set descr 'Test log(matches, to pflogX)' + atf_set require.user root +} + +matches_logif_body() +{ + pflog_init + + epair=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + ifconfig ${epair}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + jexec alcatraz ifconfig pflog0 up + jexec alcatraz ifconfig pflog1 create + jexec alcatraz ifconfig pflog1 up + pft_set_rules alcatraz \ + "match log(matches, to pflog1) inet proto icmp" \ + "match log inet from 192.0.2.2" \ + "pass log(to pflog0)" + + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog1 >> ${PWD}/pflog1.txt & + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog0.txt & + sleep 1 # Wait for tcpdump to start + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + echo "Rules" + jexec alcatraz pfctl -sr -vv + echo "States" + jexec alcatraz pfctl -ss -vv + echo "Log 0" + cat ${PWD}/pflog0.txt + echo "Log 1" + cat ${PWD}/pflog1.txt + + atf_check -o match:".*rule 0/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog1.txt + atf_check -o match:".*rule 1/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog1.txt +} + +matches_logif_cleanup() +{ + pft_cleanup +} + +atf_test_case "state_max" "cleanup" +state_max_head() +{ + atf_set descr 'Ensure that drops due to state limits are logged' + atf_set require.user root +} + +state_max_body() +{ + pflog_init + + epair=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + ifconfig ${epair}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + jexec alcatraz ifconfig pflog0 up + pft_set_rules alcatraz "pass log inet keep state (max 1)" + + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog.txt & + sleep 1 # Wait for tcpdump to start + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + atf_check -s exit:2 -o ignore \ + ping -c 1 192.0.2.1 + + echo "Rules" + jexec alcatraz pfctl -sr -vv + echo "States" + jexec alcatraz pfctl -ss -vv + echo "Log" + cat ${PWD}/pflog.txt + + # First ping passes. + atf_check -o match:".*rule 0/0\(match\): pass in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog.txt + + # Second ping is blocked due to the state limit. + atf_check -o match:".*rule 0/12\(state-limit\): block in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog.txt + + # At most three lines should be written: one for the first ping, and + # two for the second: one for the initial pass through the ruleset, and + # then a drop because of the state limit. Ideally only the drop would + # be logged; if this is fixed, the count will be 2 instead of 3. + atf_check -o match:3 grep -c . pflog.txt + + # If the rule doesn't specify logging, we shouldn't log drops + # due to state limits. + pft_set_rules alcatraz "pass inet keep state (max 1)" + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + atf_check -s exit:2 -o ignore \ + ping -c 1 192.0.2.1 + + atf_check -o match:3 grep -c . pflog.txt +} + +state_max_cleanup() +{ + pft_cleanup +} + +atf_test_case "unspecified_v4" "cleanup" +unspecified_v4_head() +{ + atf_set descr 'Ensure that packets to the unspecified address are visible to pfil hooks' + atf_set require.user root +} + +unspecified_v4_body() +{ + pflog_init + + vnet_mkjail alcatraz + jexec alcatraz ifconfig lo0 inet 127.0.0.1 + jexec alcatraz route add default 127.0.0.1 + + jexec alcatraz pfctl -e + jexec alcatraz ifconfig pflog0 up + pft_set_rules alcatraz "block log on lo0 to 0.0.0.0" + + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> pflog.txt & + sleep 1 # Wait for tcpdump to start + + atf_check -s not-exit:0 -o ignore -e ignore \ + jexec alcatraz ping -S 127.0.0.1 -c 1 0.0.0.0 + + atf_check -o match:".*: block out on lo0: 127.0.0.1 > 0.0.0.0: ICMP echo request,.*" \ + cat pflog.txt +} + +unspecified_v4_cleanup() +{ + pft_cleanup +} + +atf_test_case "unspecified_v6" "cleanup" +unspecified_v6_head() +{ + atf_set descr 'Ensure that packets to the unspecified address are visible to pfil hooks' + atf_set require.user root +} + +unspecified_v6_body() +{ + pflog_init + + vnet_mkjail alcatraz + jexec alcatraz ifconfig lo0 up + jexec alcatraz route -6 add ::0 ::1 + + jexec alcatraz pfctl -e + jexec alcatraz ifconfig pflog0 up + pft_set_rules alcatraz "block log on lo0 to ::0" + + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> pflog.txt & + sleep 1 # Wait for tcpdump to start + + atf_check -s not-exit:0 -o ignore -e ignore \ + jexec alcatraz ping -6 -S ::1 -c 1 ::0 + + cat pflog.txt + atf_check -o match:".*: block out on lo0: ::1 > ::: ICMP6, echo request,.*" \ + cat pflog.txt +} + +unspecified_v6_cleanup() +{ + pft_cleanup +} + +atf_test_case "rdr_action" "cleanup" +rdr_head() +{ + atf_set descr 'Ensure that NAT rule actions are logged correctly' + atf_set require.user root +} + +rdr_action_body() +{ + pflog_init + + j="pflog:rdr_action" + epair_c=$(vnet_mkepair) + epair_srv=$(vnet_mkepair) + + vnet_mkjail ${j}srv ${epair_srv}a + vnet_mkjail ${j}gw ${epair_srv}b ${epair_c}a + vnet_mkjail ${j}c ${epair_c}b + + jexec ${j}srv ifconfig ${epair_srv}a 198.51.100.1/24 up + # No default route in srv jail, to ensure we're NAT-ing + jexec ${j}gw ifconfig ${epair_srv}b 198.51.100.2/24 up + jexec ${j}gw ifconfig ${epair_c}a 192.0.2.1/24 up + jexec ${j}gw sysctl net.inet.ip.forwarding=1 + jexec ${j}c ifconfig ${epair_c}b 192.0.2.2/24 up + jexec ${j}c route add default 192.0.2.1 + + jexec ${j}gw pfctl -e + jexec ${j}gw ifconfig pflog0 up + pft_set_rules ${j}gw \ + "rdr log on ${epair_srv}b proto tcp from 198.51.100.0/24 to any port 1234 -> 192.0.2.2 port 1234" \ + "block quick inet6" \ + "pass in log" + + jexec ${j}gw tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog.txt & + sleep 1 # Wait for tcpdump to start + + # send a SYN to catch in the log + jexec ${j}srv nc -N -w 0 198.51.100.2 1234 + + echo "Log" + cat ${PWD}/pflog.txt + + # log line generated for rdr hit (pre-NAT) + atf_check -o match:".*.*rule 0/0\(match\): rdr in on ${epair_srv}b: 198.51.100.1.[0-9]* > 198.51.100.2.1234: Flags \[S\].*" \ + cat pflog.txt + + # log line generated for pass hit (post-NAT) + atf_check -o match:".*.*rule 1/0\(match\): pass in on ${epair_srv}b: 198.51.100.1.[0-9]* > 192.0.2.2.1234: Flags \[S\].*" \ + cat pflog.txt + + # only two log lines shall be written + atf_check -o match:2 grep -c . pflog.txt +} + +rdr_action_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "malformed" + atf_add_test_case "matches" + atf_add_test_case "matches_logif" + atf_add_test_case "state_max" + atf_add_test_case "unspecified_v4" + atf_add_test_case "unspecified_v6" + atf_add_test_case "rdr_action" } diff --git a/tests/sys/netpfil/pf/pflow.sh b/tests/sys/netpfil/pf/pflow.sh index f0552eb061da..1122096d2e31 100644 --- a/tests/sys/netpfil/pf/pflow.sh +++ b/tests/sys/netpfil/pf/pflow.sh @@ -85,7 +85,7 @@ state_defaults_head() { atf_set descr 'Test set state-defaults pflow' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } state_defaults_body() @@ -146,7 +146,7 @@ v6_head() { atf_set descr 'Test pflow over IPv6' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() @@ -188,7 +188,7 @@ nat_head() { atf_set descr 'Test pflow export for NAT44' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } nat_body() @@ -239,7 +239,7 @@ rule_head() { atf_set descr 'Test per-rule pflow option' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } rule_body() diff --git a/tests/sys/netpfil/pf/pfsync.sh b/tests/sys/netpfil/pf/pfsync.sh index 87dfcf748d3c..3be4a3024393 100644 --- a/tests/sys/netpfil/pf/pfsync.sh +++ b/tests/sys/netpfil/pf/pfsync.sh @@ -126,7 +126,7 @@ defer_head() { atf_set descr 'Defer mode pfsync test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } defer_body() @@ -335,6 +335,7 @@ pbr_common_body() atf_skip "This test requires carp" fi pfsynct_init + vnet_init_bridge bridge0=$(vnet_mkbridge) bridge1=$(vnet_mkbridge) @@ -834,21 +835,100 @@ basic_ipv6_cleanup() pfsynct_cleanup } -atf_test_case "route_to" "cleanup" -route_to_head() +atf_test_case "rtable" "cleanup" +rtable_head() { - atf_set descr 'Test route-to with default rule' + atf_set descr 'Test handling of invalid rtableid' atf_set require.user root - atf_set require.progs scapy } -route_to_body() +rtable_body() { pfsynct_init epair_sync=$(vnet_mkepair) epair_one=$(vnet_mkepair) epair_two=$(vnet_mkepair) + + vnet_mkjail one ${epair_one}a ${epair_sync}a + vnet_mkjail two ${epair_two}a ${epair_sync}b + + # pfsync interface + jexec one ifconfig ${epair_sync}a 192.0.2.1/24 up + jexec one ifconfig ${epair_one}a 198.51.100.1/24 up + jexec one ifconfig pfsync0 \ + syncdev ${epair_sync}a \ + maxupd 1 \ + up + jexec two ifconfig ${epair_two}a 198.51.100.1/24 up + jexec two ifconfig ${epair_sync}b 192.0.2.2/24 up + jexec two ifconfig pfsync0 \ + syncdev ${epair_sync}b \ + maxupd 1 \ + up + + # Make life easy, give ${epair_two}a the same mac addrss as ${epair_one}a + mac=$(jexec one ifconfig ${epair_one}a | awk '/ether/ { print($2); }') + jexec two ifconfig ${epair_two}a ether ${mac} + + # Enable pf! + jexec one /sbin/sysctl net.fibs=8 + jexec one pfctl -e + pft_set_rules one \ + "set skip on ${epair_sync}a" \ + "pass rtable 3 keep state" + # No extra fibs in two + jexec two pfctl -e + pft_set_rules two \ + "set skip on ${epair_sync}b" \ + "pass keep state" + + ifconfig ${epair_one}b 198.51.100.254/24 up + ifconfig ${epair_two}b 198.51.100.253/24 up + + # Create a new state + env PYTHONPATH=${common_dir} \ + ${common_dir}/pft_ping.py \ + --sendif ${epair_one}b \ + --fromaddr 198.51.100.254 \ + --to 198.51.100.1 \ + --recvif ${epair_one}b + + # Now + jexec one pfctl -ss -vv + sleep 2 + + # Now try to use that state on jail two + env PYTHONPATH=${common_dir} \ + ${common_dir}/pft_ping.py \ + --sendif ${epair_two}b \ + --fromaddr 198.51.100.254 \ + --to 198.51.100.1 \ + --recvif ${epair_two}b + + echo one + jexec one pfctl -ss -vv + jexec one pfctl -sr -vv + echo two + jexec two pfctl -ss -vv + jexec two pfctl -sr -vv +} + +rtable_cleanup() +{ + pfsynct_cleanup +} + +route_to_common_head() +{ + pfsync_version=$1 + shift + + pfsynct_init + + epair_sync=$(vnet_mkepair) + epair_one=$(vnet_mkepair) + epair_two=$(vnet_mkepair) epair_out_one=$(vnet_mkepair) epair_out_two=$(vnet_mkepair) @@ -865,40 +945,111 @@ route_to_body() jexec one ifconfig pfsync0 \ syncdev ${epair_sync}a \ maxupd 1 \ + version $pfsync_version \ up jexec two ifconfig ${epair_sync}b 192.0.2.2/24 up jexec two ifconfig ${epair_two}a 198.51.100.2/24 up jexec two ifconfig ${epair_out_two}a 203.0.113.2/24 up - #jexec two ifconfig ${epair_out_two}a name outif + jexec two ifconfig ${epair_out_two}a name outif jexec two sysctl net.inet.ip.forwarding=1 jexec two arp -s 203.0.113.254 00:01:02:03:04:05 jexec two ifconfig pfsync0 \ syncdev ${epair_sync}b \ maxupd 1 \ + version $pfsync_version \ up - # Enable pf! + ifconfig ${epair_one}b 198.51.100.254/24 up + ifconfig ${epair_two}b 198.51.100.253/24 up + route add -net 203.0.113.0/24 198.51.100.1 + ifconfig ${epair_two}b up + ifconfig ${epair_out_one}b up + ifconfig ${epair_out_two}b up +} + +route_to_common_tail() +{ + atf_check -s exit:0 env PYTHONPATH=${common_dir} \ + ${common_dir}/pft_ping.py \ + --sendif ${epair_one}b \ + --fromaddr 198.51.100.254 \ + --to 203.0.113.254 \ + --recvif ${epair_out_one}b + + # Allow time for sync + sleep 2 + + states_one=$(mktemp) + states_two=$(mktemp) + jexec one pfctl -qvvss | normalize_pfctl_s > $states_one + jexec two pfctl -qvvss | normalize_pfctl_s > $states_two +} + +atf_test_case "route_to_1301_body" "cleanup" +route_to_1301_head() +{ + atf_set descr 'Test route-to with pfsync version 13.1' + atf_set require.user root + atf_set require.progs python3 scapy +} + +route_to_1301_body() +{ + route_to_common_head 1301 + jexec one pfctl -e pft_set_rules one \ "set skip on ${epair_sync}a" \ "pass out route-to (outif 203.0.113.254)" + jexec two pfctl -e + pft_set_rules two \ + "set skip on ${epair_sync}b" \ + "pass out route-to (outif 203.0.113.254)" + + route_to_common_tail + + # Sanity check + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one || + atf_fail "State missing on router one" + + # With identical ruleset the routing information is recovered from the matching rule. + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif' $states_two || + atf_fail "State missing on router two" + + true +} + +route_to_1301_cleanup() +{ + pfsynct_cleanup +} + +atf_test_case "route_to_1301_bad_ruleset" "cleanup" +route_to_1301_bad_ruleset_head() +{ + atf_set descr 'Test route-to with pfsync version 13.1 and incompatible ruleset' + atf_set require.user root + atf_set require.progs python3 scapy +} + +route_to_1301_bad_ruleset_body() +{ + route_to_common_head 1301 - # Make sure we have different rulesets so the synced state is associated with - # V_pf_default_rule + jexec one pfctl -e + pft_set_rules one \ + "set skip on ${epair_sync}a" \ + "pass out route-to (outif 203.0.113.254)" + + jexec two pfctl -e pft_set_rules two \ + "set debug loud" \ "set skip on ${epair_sync}b" \ "pass out route-to (outif 203.0.113.254)" \ "pass out proto tcp" - ifconfig ${epair_one}b 198.51.100.254/24 up - ifconfig ${epair_two}b 198.51.100.253/24 up - route add -net 203.0.113.0/24 198.51.100.1 - ifconfig ${epair_two}b up - ifconfig ${epair_out_one}b up - ifconfig ${epair_out_two}b up - atf_check -s exit:0 env PYTHONPATH=${common_dir} \ ${common_dir}/pft_ping.py \ --sendif ${epair_one}b \ @@ -906,25 +1057,151 @@ route_to_body() --to 203.0.113.254 \ --recvif ${epair_out_one}b - # Allow time for sync - ifconfig ${epair_one}b inet 198.51.100.254 -alias - route del -net 203.0.113.0/24 198.51.100.1 - route add -net 203.0.113.0/24 198.51.100.2 + route_to_common_tail - sleep 2 + # Sanity check + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one || + atf_fail "State missing on router one" - # Now try to trigger the state on the other pfsync member - env PYTHONPATH=${common_dir} \ + # Different ruleset on each router means the routing information recovery + # from rule is impossible. The state is not synced. + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*' $states_two && + atf_fail "State present on router two" + + true +} + +route_to_1301_bad_ruleset_cleanup() +{ + pfsynct_cleanup +} + +atf_test_case "route_to_1301_bad_rpool" "cleanup" +route_to_1301_bad_rpool_head() +{ + atf_set descr 'Test route-to with pfsync version 13.1 and different interface' + atf_set require.user root + atf_set require.progs python3 scapy +} + +route_to_1301_bad_rpool_body() +{ + route_to_common_head 1301 + + jexec one pfctl -e + pft_set_rules one \ + "set skip on ${epair_sync}a" \ + "pass out route-to { (outif 203.0.113.254) (outif 203.0.113.254) }" + + jexec two pfctl -e + pft_set_rules two \ + "set skip on ${epair_sync}b" \ + "pass out route-to { (outif 203.0.113.254) (outif 203.0.113.254) }" + + atf_check -s exit:0 env PYTHONPATH=${common_dir} \ ${common_dir}/pft_ping.py \ - --sendif ${epair_two}b \ + --sendif ${epair_one}b \ --fromaddr 198.51.100.254 \ --to 203.0.113.254 \ - --recvif ${epair_out_two}b + --recvif ${epair_out_one}b + + route_to_common_tail + + # Sanity check + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one || + atf_fail "State missing on router one" + + # The ruleset is identical but since the redirection pool contains multiple interfaces + # pfsync will not attempt to recover the routing information from the rule. + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*' $states_two && + atf_fail "State present on router two" + + true +} + +route_to_1301_bad_rpool_cleanup() +{ + pfsynct_cleanup +} + +atf_test_case "route_to_1400_bad_ruleset" "cleanup" +route_to_1400_bad_ruleset_head() +{ + atf_set descr 'Test route-to with pfsync version 14.0' + atf_set require.user root + atf_set require.progs python3 scapy +} + +route_to_1400_bad_ruleset_body() +{ + route_to_common_head 1400 + + jexec one pfctl -e + pft_set_rules one \ + "set skip on ${epair_sync}a" \ + "pass out route-to (outif 203.0.113.254)" + + jexec two pfctl -e + pft_set_rules two \ + "set skip on ${epair_sync}b" + + route_to_common_tail + + # Sanity check + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one || + atf_fail "State missing on router one" + + # Even with a different ruleset FreeBSD 14 syncs the state just fine. + # There's no recovery involved, the pfsync packet contains the routing information. + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .* route-to: 203.0.113.254@outif' $states_two || + atf_fail "State missing on router two" + + true +} + +route_to_1400_bad_ruleset_cleanup() +{ + pfsynct_cleanup +} + +atf_test_case "route_to_1400_bad_ifname" "cleanup" +route_to_1400_bad_ifname_head() +{ + atf_set descr 'Test route-to with pfsync version 14.0' + atf_set require.user root + atf_set require.progs python3 scapy +} + +route_to_1400_bad_ifname_body() +{ + route_to_common_head 1400 + + jexec one pfctl -e + pft_set_rules one \ + "set skip on ${epair_sync}a" \ + "pass out route-to (outif 203.0.113.254)" + + jexec two pfctl -e + jexec two ifconfig outif name outif_new + pft_set_rules two \ + "set skip on ${epair_sync}b" \ + "pass out route-to (outif_new 203.0.113.254)" + + route_to_common_tail + + # Sanity check + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one || + atf_fail "State missing on router one" + + # Since FreeBSD 14 never attempts recovery of missing routing information + # a state synced to a router with a different interface name is dropped. + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*' $states_two && + atf_fail "State present on router two" true } -route_to_cleanup() +route_to_1400_bad_ifname_cleanup() { pfsynct_cleanup } @@ -941,5 +1218,10 @@ atf_init_test_cases() atf_add_test_case "timeout" atf_add_test_case "basic_ipv6_unicast" atf_add_test_case "basic_ipv6" - atf_add_test_case "route_to" + atf_add_test_case "rtable" + atf_add_test_case "route_to_1301" + atf_add_test_case "route_to_1301_bad_ruleset" + atf_add_test_case "route_to_1301_bad_rpool" + atf_add_test_case "route_to_1400_bad_ruleset" + atf_add_test_case "route_to_1400_bad_ifname" } diff --git a/tests/sys/netpfil/pf/pft_read_ipfix.py b/tests/sys/netpfil/pf/pft_read_ipfix.py index 2c11bdfd130c..02ef2ca5ab06 100644 --- a/tests/sys/netpfil/pf/pft_read_ipfix.py +++ b/tests/sys/netpfil/pf/pft_read_ipfix.py @@ -60,7 +60,7 @@ def parse_ipfix(p): c = datafl.payload def receive(recvif, recvport): - pkts = sp.sniff(iface=recvif, timeout=65) + pkts = sp.sniff(iface=recvif, timeout=65, filter="udp port 2055") if len(pkts) == 0: print("No data") diff --git a/tests/sys/netpfil/pf/proxy.sh b/tests/sys/netpfil/pf/proxy.sh index 4a7ea00a0cd4..78ce25930c04 100644 --- a/tests/sys/netpfil/pf/proxy.sh +++ b/tests/sys/netpfil/pf/proxy.sh @@ -57,7 +57,7 @@ ftp_body() jexec srv route add default 198.51.100.1 # Start FTP server in srv - jexec srv twistd ftp -r `pwd` -p 21 + jexec srv twistd --logfile=/dev/null ftp -r `pwd` -p 21 # Sanity check atf_check -s exit:0 -o ignore ping -c 1 198.51.100.2 @@ -74,7 +74,7 @@ ftp_body() # Create a dummy file to download echo 'foo' > remote.txt - echo 'get remote.txt local.txt' | ftp -a 198.51.100.2 + echo -e 'epsv\nget remote.txt local.txt' | ftp -a 198.51.100.2 # Compare the downloaded file to the original if ! diff -q local.txt remote.txt; diff --git a/tests/sys/netpfil/pf/rdr-srcport.py b/tests/sys/netpfil/pf/rdr-srcport.py new file mode 100644 index 000000000000..633580582711 --- /dev/null +++ b/tests/sys/netpfil/pf/rdr-srcport.py @@ -0,0 +1,20 @@ +# +# A helper script which accepts TCP connections and writes the remote port +# number to the stream. +# + +import socket + +def main(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('0.0.0.0', 8888)) + s.listen(5) + + while True: + cs, addr = s.accept() + cs.sendall(str(addr[1]).encode()) + cs.close() + +if __name__ == '__main__': + main() diff --git a/tests/sys/netpfil/pf/rdr.sh b/tests/sys/netpfil/pf/rdr.sh index 5e60b97c653b..f7c920bbfa8f 100644 --- a/tests/sys/netpfil/pf/rdr.sh +++ b/tests/sys/netpfil/pf/rdr.sh @@ -27,14 +27,6 @@ . $(atf_get_srcdir)/utils.subr -atf_test_case "tcp_v6" "cleanup" -tcp_v6_head() -{ - atf_set descr 'TCP rdr with IPv6' - atf_set require.user root - atf_set require.progs scapy python3 -} - # # Test that rdr works for TCP with IPv6. # @@ -47,7 +39,7 @@ tcp_v6_head() # # Test for incorrect checksums after the rewrite by looking at a packet capture (see bug 210860) # -tcp_v6_body() +tcp_v6_setup() { pft_init @@ -83,16 +75,18 @@ tcp_v6_body() jexec ${j}c route add -inet6 2001:db8:a::0/64 2001:db8:b::1 jexec ${j}b pfctl -e +} - pft_set_rules ${j}b \ - "rdr on ${epair_one}a proto tcp from any to any port 80 -> 2001:db8:b::2 port 8000" +tcp_v6_common() +{ + pft_set_rules ${j}b "${1}" # Check that a can reach c over the router atf_check -s exit:0 -o ignore \ jexec ${j}a ping -6 -c 1 2001:db8:b::2 # capture packets on c so we can look for incorrect checksums - jexec ${j}c tcpdump --immediate-mode -w ${j}.pcap tcp and port 8000 & + jexec ${j}c tcpdump --immediate-mode -w ${PWD}/${j}.pcap tcp and port 8000 & tcpdumppid=$! # start a web server and give it a second to start @@ -112,16 +106,185 @@ tcp_v6_body() # Check for 'incorrect' in packet capture, this should tell us if # checksums are bad with rdr rules - count=$(jexec ${j}c tcpdump -vvvv -r ${j}.pcap | grep incorrect | wc -l) + count=$(jexec ${j}c tcpdump -vvvv -r ${PWD}/${j}.pcap | grep incorrect | wc -l) atf_check_equal " 0" "$count" } -tcp_v6_cleanup() +atf_test_case "tcp_v6_compat" "cleanup" +tcp_v6_compat_head() +{ + atf_set descr 'TCP rdr with IPv6 with NAT rules' + atf_set require.user root + atf_set require.progs python3 +} + +tcp_v6_compat_body() +{ + tcp_v6_setup # Sets ${epair_…} variables + tcp_v6_common \ + "rdr on ${epair_one}a proto tcp from any to any port 80 -> 2001:db8:b::2 port 8000" +} + +tcp_v6_compat_cleanup() +{ + pft_cleanup +} + +atf_test_case "tcp_v6_pass" "cleanup" +tcp_v6_pass_head() +{ + atf_set descr 'TCP rdr with IPv6 with pass/match rules' + atf_set require.user root + atf_set require.progs python3 +} + +tcp_v6_pass_body() +{ + tcp_v6_setup # Sets ${epair_…} variables + tcp_v6_common \ + "pass in on ${epair_one}a proto tcp from any to any port 80 rdr-to 2001:db8:b::2 port 8000" +} + +tcp_v6_pass_cleanup() +{ + pft_cleanup +} + +# +# Test that rdr works for multiple TCP with same srcip and srcport. +# +# Four jails, a, b, c, d, are used: +# - jail d runs a server on port 8888, +# - jail a makes connections to the server, routed through jails b and c, +# - jail b uses NAT to rewrite source addresses and ports to the same 2-tuple, +# avoiding the need to use SO_REUSEADDR in jail a, +# - jail c uses a redirect rule to map the destination address to the same +# address and port, resulting in a NAT state conflict. +# +# In this case, the rdr rule should also rewrite the source port (again) to +# resolve the state conflict. +# +srcport_setup() +{ + pft_init + + j="rdr:srcport" + epair1=$(vnet_mkepair) + epair2=$(vnet_mkepair) + epair3=$(vnet_mkepair) + + echo $epair_one + echo $epair_two + + vnet_mkjail ${j}a ${epair1}a + vnet_mkjail ${j}b ${epair1}b ${epair2}a + vnet_mkjail ${j}c ${epair2}b ${epair3}a + vnet_mkjail ${j}d ${epair3}b + + # configure addresses for a + jexec ${j}a ifconfig lo0 up + jexec ${j}a ifconfig ${epair1}a inet 198.51.100.50/24 up + jexec ${j}a ifconfig ${epair1}a inet alias 198.51.100.51/24 + jexec ${j}a ifconfig ${epair1}a inet alias 198.51.100.52/24 + + # configure addresses for b + jexec ${j}b ifconfig lo0 up + jexec ${j}b ifconfig ${epair1}b inet 198.51.100.1/24 up + jexec ${j}b ifconfig ${epair2}a inet 198.51.101.2/24 up + + # configure addresses for c + jexec ${j}c ifconfig lo0 up + jexec ${j}c ifconfig ${epair2}b inet 198.51.101.3/24 up + jexec ${j}c ifconfig ${epair2}b inet alias 198.51.101.4/24 + jexec ${j}c ifconfig ${epair2}b inet alias 198.51.101.5/24 + jexec ${j}c ifconfig ${epair3}a inet 203.0.113.1/24 up + + # configure addresses for d + jexec ${j}d ifconfig lo0 up + jexec ${j}d ifconfig ${epair3}b inet 203.0.113.50/24 up + + jexec ${j}b sysctl net.inet.ip.forwarding=1 + jexec ${j}c sysctl net.inet.ip.forwarding=1 + jexec ${j}b pfctl -e + jexec ${j}c pfctl -e +} + +srcport_common() +{ + pft_set_rules ${j}b \ + "set debug misc" \ + "${1}" + + pft_set_rules ${j}c \ + "set debug misc" \ + "${2}" + + jexec ${j}a route add default 198.51.100.1 + jexec ${j}c route add 198.51.100.0/24 198.51.101.2 + jexec ${j}d route add 198.51.101.0/24 203.0.113.1 + + jexec ${j}d python3 $(atf_get_srcdir)/rdr-srcport.py & + sleep 1 + + echo a | jexec ${j}a nc -w 3 -s 198.51.100.50 -p 1234 198.51.101.3 7777 > port1 + + jexec ${j}a nc -s 198.51.100.51 -p 1234 198.51.101.4 7777 > port2 & + jexec ${j}a nc -s 198.51.100.52 -p 1234 198.51.101.5 7777 > port3 & + sleep 1 + + atf_check -o inline:"1234" cat port1 + atf_check -o match:"[0-9]+" -o not-inline:"1234" cat port2 + atf_check -o match:"[0-9]+" -o not-inline:"1234" cat port3 +} + +atf_test_case "srcport_compat" "cleanup" +srcport_compat_head() +{ + atf_set descr 'TCP rdr srcport modulation with NAT rules' + atf_set require.user root + atf_set require.progs python3 + atf_set timeout 9999 +} + +srcport_compat_body() +{ + srcport_setup # Sets ${epair_…} variables + srcport_common \ + "nat on ${epair2}a inet from 198.51.100.0/24 to any -> ${epair2}a static-port" \ + "rdr on ${epair2}b proto tcp from any to ${epair2}b port 7777 -> 203.0.113.50 port 8888" +} + +srcport_compat_cleanup() +{ + pft_cleanup +} + +atf_test_case "srcport_pass" "cleanup" +srcport_pass_head() +{ + atf_set descr 'TCP rdr srcport modulation with pass/match rules' + atf_set require.user root + atf_set require.progs python3 + atf_set timeout 9999 +} + +srcport_pass_body() +{ + srcport_setup # Sets ${epair_…} variables + srcport_common \ + "pass out on ${epair2}a inet from 198.51.100.0/24 to any nat-to ${epair2}a static-port" \ + "pass in on ${epair2}b proto tcp from any to ${epair2}b port 7777 rdr-to 203.0.113.50 port 8888" +} + +srcport_pass_cleanup() { pft_cleanup } atf_init_test_cases() { - atf_add_test_case "tcp_v6" + atf_add_test_case "tcp_v6_compat" + atf_add_test_case "tcp_v6_pass" + atf_add_test_case "srcport_compat" + atf_add_test_case "srcport_pass" } diff --git a/tests/sys/netpfil/pf/return.py b/tests/sys/netpfil/pf/return.py new file mode 100644 index 000000000000..753012860764 --- /dev/null +++ b/tests/sys/netpfil/pf/return.py @@ -0,0 +1,153 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Rubicon Communications, LLC (Netgate) +# +# 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. +# +import pytest +import subprocess +import re +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +def check(cmd): + ps = subprocess.Popen(cmd, shell=True) + ret = ps.wait() + if ret != 0: + raise Exception("Command %s returned %d" % (cmd, ret)) + +class TestReturn(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2"]}, + "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]}, + "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]}, + } + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + if2name = vnet.iface_alias_map["if2"].name + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "nat on %s inet from 192.0.2.0/24 to any -> (%s)" % (ifname, ifname), + "block return", + "pass inet proto icmp icmp-type echoreq", + ]) + + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + ToolsHelper.print_output("/sbin/pfctl -x loud") + + def vnet3_handler(self, vnet): + ToolsHelper.print_output("/sbin/route add default 198.51.100.1") + + def common_setup(self): + ToolsHelper.print_output("/sbin/route add default 192.0.2.1") + + # Sanity check + check("/sbin/ping -c 1 192.0.2.1") + check("/sbin/ping -c 1 198.51.100.1") + check("/sbin/ping -c 2 198.51.100.2") + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_tcp(self): + self.common_setup() + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Send a TCP SYN, expect a RST + pkt = sp.IP(src="192.0.2.2", dst="198.51.100.2") \ + / sp.TCP(sport=4321, dport=1234, flags="S") + print(pkt) + reply = sp.sr1(pkt, timeout=3, verbose=False) + print(reply) + + ip = reply.getlayer(sp.IP) + tcp = reply.getlayer(sp.TCP) + assert ip + assert ip.src == "198.51.100.2" + assert ip.dst == "192.0.2.2" + assert tcp + assert tcp.sport == 1234 + assert tcp.dport == 4321 + assert "R" in tcp.flags + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_udp(self): + self.common_setup() + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Send a UDP packet, expect ICMP error + pkt = sp.IP(dst="198.51.100.2") \ + / sp.UDP(sport=4321, dport=1234) + print(pkt) + reply = sp.sr1(pkt, timeout=3, verbose=False) + print(reply) + ip = reply.getlayer(sp.IP) + icmp = reply.getlayer(sp.ICMP) + udp = reply.getlayer(sp.UDPerror) + + assert ip + assert ip.src == "192.0.2.1" + assert ip.dst == "192.0.2.2" + assert icmp + assert icmp.type == 3 + assert icmp.code == 3 + assert udp + assert udp.sport == 4321 + assert udp.dport == 1234 + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_sctp(self): + self.common_setup() + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Send an SCTP init, expect an SCTP abort + pkt = sp.IP(dst="198.51.100.2") \ + / sp.SCTP(sport=1111, dport=2222) \ + / sp.SCTPChunkInit(init_tag=1, n_in_streams=1, n_out_streams=1, a_rwnd=1500) + print(pkt) + reply = sp.sr1(pkt, timeout=3, verbose=False) + print(reply) + ip = reply.getlayer(sp.IP) + sctp = reply.getlayer(sp.SCTP) + abort = reply.getlayer(sp.SCTPChunkAbort) + print(sctp) + + assert ip + assert ip.src == "198.51.100.2" + assert ip.dst == "192.0.2.2" + assert sctp + assert sctp.sport == 2222 + assert sctp.dport == 1111 + assert(abort) diff --git a/tests/sys/netpfil/pf/ridentifier.sh b/tests/sys/netpfil/pf/ridentifier.sh index c456d2111e20..8d83bcfb8213 100644 --- a/tests/sys/netpfil/pf/ridentifier.sh +++ b/tests/sys/netpfil/pf/ridentifier.sh @@ -45,7 +45,7 @@ basic_body() vnet_mkjail alcatraz ${epair}b jexec alcatraz ifconfig lo0 up jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid $(atf_get_srcdir)/echo_inetd.conf + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid $(atf_get_srcdir)/echo_inetd.conf # Sanity check atf_check -s exit:0 -o ignore ping -c 1 192.0.2.2 @@ -56,7 +56,7 @@ basic_body() "pass in log" \ "pass in log proto tcp ridentifier 1234" - jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > tcpdump.log & + jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > ${PWD}/tcpdump.log & sleep 1 echo "test" | nc -N 192.0.2.2 7 @@ -67,17 +67,17 @@ basic_body() # Make sure we spotted the ridentifier atf_check -s exit:0 -o ignore \ - grep 'rule 1/0.*ridentifier 1234' tcpdump.log + grep 'rule 1/0.*ridentifier 1234' ${PWD}/tcpdump.log # But not on the !TCP traffic atf_check -s exit:1 -o ignore \ - grep 'rule 0/0.*ridentifier' tcpdump.log + grep 'rule 0/0.*ridentifier' ${PWD}/tcpdump.log # Now try with antispoof rules pft_set_rules alcatraz \ "pass in log" \ "antispoof log for ${epair}b ridentifier 4321" - jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > tcpdump.log & + jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > ${PWD}/tcpdump.log & sleep 1 # Without explicit rules for lo0 we're going to drop packets to ourself @@ -87,18 +87,16 @@ basic_body() sleep 1 jexec alcatraz killall tcpdump - cat tcpdump.log + cat ${PWD}/tcpdump.log # Make sure we spotted the ridentifier atf_check -s exit:0 -o ignore \ - grep 'rule 2/0.*ridentifier 4321' tcpdump.log + grep 'rule 2/0.*ridentifier 4321' ${PWD}/tcpdump.log } basic_cleanup() { pft_cleanup - rm -f inetd-alcatraz.pid - rm -f tcpdump.log } atf_init_test_cases() diff --git a/tests/sys/netpfil/pf/route_to.sh b/tests/sys/netpfil/pf/route_to.sh index df95eaecc12e..fd1653cce311 100644 --- a/tests/sys/netpfil/pf/route_to.sh +++ b/tests/sys/netpfil/pf/route_to.sh @@ -140,7 +140,7 @@ multiwan_body() jexec srv sysctl net.inet.ip.forwarding=1 # Run echo server in srv jail - jexec srv /usr/sbin/inetd -p multiwan.pid $(atf_get_srcdir)/echo_inetd.conf + jexec srv /usr/sbin/inetd -p ${PWD}/multiwan.pid $(atf_get_srcdir)/echo_inetd.conf jexec srv pfctl -e pft_set_rules srv \ @@ -178,7 +178,6 @@ multiwan_body() multiwan_cleanup() { - rm -f multiwan.pid pft_cleanup } @@ -257,7 +256,7 @@ icmp_nat_head() { atf_set descr 'Test that ICMP packets are correct for route-to + NAT' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } icmp_nat_body() @@ -517,6 +516,7 @@ ifbound_reply_to_head() { atf_set descr 'Test that reply-to states bind to the expected interface' atf_set require.user root + atf_set require.progs python3 scapy } ifbound_reply_to_body() @@ -572,6 +572,7 @@ ifbound_reply_to_v6_head() { atf_set descr 'Test that reply-to states bind to the expected interface for IPv6' atf_set require.user root + atf_set require.progs python3 scapy } ifbound_reply_to_v6_body() @@ -631,6 +632,7 @@ ifbound_reply_to_rdr_dummynet_head() { atf_set descr 'Test that reply-to states bind to the expected non-default-route interface after rdr and dummynet' atf_set require.user root + atf_set require.progs python3 scapy } ifbound_reply_to_rdr_dummynet_body() @@ -787,6 +789,191 @@ dummynet_double_cleanup() pft_cleanup } +atf_test_case "sticky" "cleanup" +sticky_head() +{ + atf_set descr 'Set and retrieve a rule with sticky-address' + atf_set require.user root +} + +sticky_body() +{ + pft_init + + vnet_mkjail alcatraz + + pft_set_rules alcatraz \ + "pass in quick log on n_test_h_rtr route-to (n_srv_h_rtr <change_dst>) sticky-address from any to <dst> keep state" + + jexec alcatraz pfctl -qvvsr +} + +sticky_cleanup() +{ + pft_cleanup +} + +atf_test_case "ttl" "cleanup" +ttl_head() +{ + atf_set descr 'Ensure we decrement TTL on route-to' + atf_set require.user root +} + +ttl_body() +{ + pft_init + + epair_one=$(vnet_mkepair) + epair_two=$(vnet_mkepair) + ifconfig ${epair_one}b 192.0.2.2/24 up + route add default 192.0.2.1 + + vnet_mkjail alcatraz ${epair_one}a ${epair_two}a + jexec alcatraz ifconfig ${epair_one}a 192.0.2.1/24 up + jexec alcatraz ifconfig ${epair_two}a 198.51.100.1/24 up + jexec alcatraz sysctl net.inet.ip.forwarding=1 + + vnet_mkjail singsing ${epair_two}b + jexec singsing ifconfig ${epair_two}b 198.51.100.2/24 up + jexec singsing route add default 198.51.100.1 + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 3 198.51.100.2 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass out" \ + "pass in route-to (${epair_two}a 198.51.100.2)" + + atf_check -s exit:0 -o ignore \ + ping -c 3 198.51.100.2 + + atf_check -s exit:2 -o ignore \ + ping -m 1 -c 3 198.51.100.2 +} + +ttl_cleanup() +{ + pft_cleanup +} + + +atf_test_case "empty_pool" "cleanup" +empty_pool_head() +{ + atf_set descr 'Route-to with empty pool' + atf_set require.user root +} + +empty_pool_body() +{ + pft_init + setup_router_server_ipv6 + + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b route-to (${epair_server}a <nonexistent>) inet6 from any to ${net_server_host_server}" \ + "pass out on ${epair_server}a" + + # pf_map_addr_sn() won't be able to pick a target address, because + # the table used in redireciton pool is empty. Packet will not be + # forwarded, error counter will be increased. + ping_server_check_reply exit:1 + # Ignore warnings about not-loaded ALTQ + atf_check -o "match:map-failed +1 +" -x "jexec router pfctl -qvvsi 2> /dev/null" +} + +empty_pool_cleanup() +{ + pft_cleanup +} + + +atf_test_case "table_loop" "cleanup" + +table_loop_head() +{ + atf_set descr 'Check that iterating over tables poperly loops' + atf_set require.user root +} + +table_loop_body() +{ + setup_router_server_nat64 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses. + jexec router route add -6 ${net_clients_6}::/${net_clients_6_mask} ${net_tester_6_host_tester} + jexec router route add ${net_clients_4}.0/${net_clients_4_mask} ${net_tester_4_host_tester} + + # The servers are reachable over additional IP addresses for + # testing of tables and subnets. The addresses are noncontinougnus + # for pf_map_addr() counter tests. + for i in 0 1 4 5; do + a1=$((24 + i)) + jexec server1 ifconfig ${epair_server1}b inet ${net_server1_4}.${a1}/32 alias + jexec server1 ifconfig ${epair_server1}b inet6 ${net_server1_6}::42:${i}/128 alias + a2=$((40 + i)) + jexec server2 ifconfig ${epair_server2}b inet ${net_server2_4}.${a2}/32 alias + jexec server2 ifconfig ${epair_server2}b inet6 ${net_server2_6}::42:${i}/128 alias + done + + jexec router pfctl -e + pft_set_rules router \ + "set debug loud" \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "table <rt_targets_1> { ${net_server1_6}::42:4/127 ${net_server1_6}::42:0/127 }" \ + "table <rt_targets_2> { ${net_server2_6}::42:4/127 }" \ + "pass in on ${epair_tester}b \ + route-to { \ + (${epair_server1}a <rt_targets_1>) \ + (${epair_server2}a <rt_targets_2_empty>) \ + (${epair_server2}a <rt_targets_2>) \ + } \ + inet6 proto tcp \ + keep state" + + # Both hosts of the pool are tables. Each table gets iterated over once, + # then the pool iterates to the next host, which is also iterated, + # then the pool loops back to the 1st host. If an empty table is found, + # it is skipped. Unless that's the only table, that is tested by + # the "empty_pool" test. + for port in $(seq 1 7); do + port=$((4200 + port)) + atf_check -s exit:0 ${common_dir}/pft_ping.py \ + --sendif ${epair_tester}a --replyif ${epair_tester}a \ + --fromaddr ${net_clients_6}::1 --to ${host_server_6} \ + --ping-type=tcp3way --send-sport=${port} + done + + states=$(mktemp) || exit 1 + jexec router pfctl -qvvss | normalize_pfctl_s > $states + cat $states + + for state_regexp in \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4201\] .* route-to: ${net_server1_6}::42:0@${epair_server1}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4202\] .* route-to: ${net_server1_6}::42:1@${epair_server1}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4203\] .* route-to: ${net_server1_6}::42:4@${epair_server1}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4204\] .* route-to: ${net_server1_6}::42:5@${epair_server1}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4205\] .* route-to: ${net_server2_6}::42:4@${epair_server2}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4206\] .* route-to: ${net_server2_6}::42:5@${epair_server2}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4207\] .* route-to: ${net_server1_6}::42:0@${epair_server1}a" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done +} + +table_loop_cleanup() +{ + pft_cleanup +} + + atf_init_test_cases() { atf_add_test_case "v4" @@ -803,4 +990,8 @@ atf_init_test_cases() atf_add_test_case "ifbound_reply_to_rdr_dummynet" atf_add_test_case "dummynet_frag" atf_add_test_case "dummynet_double" + atf_add_test_case "sticky" + atf_add_test_case "ttl" + atf_add_test_case "empty_pool" + atf_add_test_case "table_loop" } diff --git a/tests/sys/netpfil/pf/rtable.sh b/tests/sys/netpfil/pf/rtable.sh index 62b37462e948..bb2cada57049 100644 --- a/tests/sys/netpfil/pf/rtable.sh +++ b/tests/sys/netpfil/pf/rtable.sh @@ -31,7 +31,7 @@ forward_v4_head() { atf_set descr 'Test IPv4 forwarding with rtable' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } forward_v4_body() @@ -78,7 +78,7 @@ forward_v6_head() { atf_set descr 'Test IPv6 forwarding with rtable' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } forward_v6_body() diff --git a/tests/sys/netpfil/pf/rules_counter.sh b/tests/sys/netpfil/pf/rules_counter.sh index 962e7c93ba93..98f96a7adca1 100644 --- a/tests/sys/netpfil/pf/rules_counter.sh +++ b/tests/sys/netpfil/pf/rules_counter.sh @@ -148,6 +148,54 @@ keepcounters_body() jexec alcatraz pfctl -s r -v } +atf_test_case "4G" "cleanup" +4G_head() +{ + atf_set descr 'Test keepcounter for values above 32 bits' + atf_set require.user root +} + +4G_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a 192.0.2.1/24 up + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz nc -l 1234 >/dev/null & + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass all" + + # Now pass more than 4GB of data + dd if=/dev/zero bs=1k count=4M | nc -N 192.0.2.2 1234 + + bytes=$(jexec alcatraz pfctl -s r -v | awk '/Bytes:/ { print $7; }') + if [ $bytes -lt 4000000000 ]; + then + atf_fail "Expected to see > 4GB" + fi + + # Set new rules, keeping counters + pft_set_rules noflush alcatraz \ + "set keepcounters" \ + "pass all" + + bytes=$(jexec alcatraz pfctl -s r -v | awk '/Bytes:/ { print $7; }') + if [ $bytes -lt 4000000000 ]; + then + atf_fail "Expected to see > 4GB after rule reload" + fi +} + +4G_cleanup() +{ + pft_cleanup +} + keepcounters_cleanup() { pft_cleanup @@ -157,4 +205,5 @@ atf_init_test_cases() { atf_add_test_case "get_clear" atf_add_test_case "keepcounters" + atf_add_test_case "4G" } diff --git a/tests/sys/netpfil/pf/scrub.sh b/tests/sys/netpfil/pf/scrub.sh index b9efdaf5205c..6a5b748bed7b 100644 --- a/tests/sys/netpfil/pf/scrub.sh +++ b/tests/sys/netpfil/pf/scrub.sh @@ -32,7 +32,7 @@ max_mss_v4_head() { atf_set descr 'Test IPv4 scrub "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v4_body() @@ -57,7 +57,7 @@ max_mss_v6_head() { atf_set descr 'Test IPv6 scrub "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v6_body() @@ -82,7 +82,7 @@ set_tos_v4_head() { atf_set descr 'Test IPv4 scub "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v4_body() @@ -103,7 +103,7 @@ set_tos_v6_head() { atf_set descr 'Test IPv6 scub "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v6_body() @@ -124,7 +124,7 @@ min_ttl_v4_head() { atf_set descr 'Test IPv4 scub "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v4_body() @@ -145,7 +145,7 @@ min_ttl_v6_head() { atf_set descr 'Test IPv6 scub "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v6_body() @@ -166,7 +166,7 @@ no_scrub_v4_head() { atf_set descr 'Test IPv4 "no scrub" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } no_scrub_v4_body() @@ -189,7 +189,7 @@ no_scrub_v6_head() { atf_set descr 'Test IPv6 "no scrub" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } no_scrub_v6_body() diff --git a/tests/sys/netpfil/pf/scrub_compat.sh b/tests/sys/netpfil/pf/scrub_compat.sh index cf69da3f3b74..6e1499309869 100644 --- a/tests/sys/netpfil/pf/scrub_compat.sh +++ b/tests/sys/netpfil/pf/scrub_compat.sh @@ -33,7 +33,7 @@ max_mss_v4_head() { atf_set descr 'Test IPv4 scrub "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v4_body() @@ -58,7 +58,7 @@ max_mss_v6_head() { atf_set descr 'Test IPv6 scrub "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v6_body() @@ -83,7 +83,7 @@ set_tos_v4_head() { atf_set descr 'Test IPv4 scub "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v4_body() @@ -104,7 +104,7 @@ set_tos_v6_head() { atf_set descr 'Test IPv6 scub "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v6_body() @@ -125,7 +125,7 @@ min_ttl_v4_head() { atf_set descr 'Test IPv4 scub "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v4_body() @@ -146,7 +146,7 @@ min_ttl_v6_head() { atf_set descr 'Test IPv6 scub "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v6_body() @@ -167,7 +167,7 @@ no_scrub_v4_head() { atf_set descr 'Test IPv4 "no scrub" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } no_scrub_v4_body() @@ -190,7 +190,7 @@ no_scrub_v6_head() { atf_set descr 'Test IPv6 "no scrub" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } no_scrub_v6_body() diff --git a/tests/sys/netpfil/pf/scrub_pass.sh b/tests/sys/netpfil/pf/scrub_pass.sh index 319d805144a0..8ba599144757 100644 --- a/tests/sys/netpfil/pf/scrub_pass.sh +++ b/tests/sys/netpfil/pf/scrub_pass.sh @@ -33,7 +33,7 @@ max_mss_v4_head() { atf_set descr 'Test IPv4 pass "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v4_body() @@ -58,7 +58,7 @@ max_mss_v6_head() { atf_set descr 'Test IPv6 pass "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v6_body() @@ -83,7 +83,7 @@ set_tos_v4_head() { atf_set descr 'Test IPv4 pass "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v4_body() @@ -104,7 +104,7 @@ set_tos_v6_head() { atf_set descr 'Test IPv6 pass "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v6_body() @@ -125,7 +125,7 @@ min_ttl_v4_head() { atf_set descr 'Test IPv4 pass "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v4_body() @@ -146,7 +146,7 @@ min_ttl_v6_head() { atf_set descr 'Test IPv6 pass "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v6_body() diff --git a/tests/sys/netpfil/pf/sctp.py b/tests/sys/netpfil/pf/sctp.py index 6042badffb64..da42ce527195 100644 --- a/tests/sys/netpfil/pf/sctp.py +++ b/tests/sys/netpfil/pf/sctp.py @@ -268,7 +268,8 @@ class TestSCTP(VnetTestTemplate): ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ "block proto sctp", - "pass inet proto sctp to 192.0.2.0/24"]) + "pass inet proto sctp to 192.0.2.0/24", + "pass on lo"]) # Sanity check, we can communicate with the primary address. client = SCTPClient("192.0.2.3", 1234) @@ -305,6 +306,7 @@ class TestSCTP(VnetTestTemplate): ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ "block proto sctp", + "pass on lo", "pass inet proto sctp from 192.0.2.0/24"]) # Sanity check, we can communicate with the primary address. @@ -362,7 +364,7 @@ class TestSCTP(VnetTestTemplate): @pytest.mark.require_user("root") - def test_permutation(self): + def test_permutation_if_bound(self): # Test that we generate all permutations of src/dst addresses. # Assign two addresses to each end, and check for the expected states srv_vnet = self.vnet_map["vnet2"] @@ -374,6 +376,7 @@ class TestSCTP(VnetTestTemplate): ToolsHelper.pf_rules([ "set state-policy if-bound", "block proto sctp", + "pass on lo", "pass inet proto sctp to 192.0.2.0/24"]) # Sanity check, we can communicate with the primary address. @@ -387,11 +390,146 @@ class TestSCTP(VnetTestTemplate): # Check that we have a state for 192.0.2.3 and 192.0.2.2 to 192.0.2.1, but also to 192.0.2.4 states = ToolsHelper.get_output("/sbin/pfctl -ss") print(states) - assert re.search(r".*sctp 192.0.2.1:.*192.0.2.3:1234", states) + assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.3:1234", states) + assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.2:1234", states) + assert re.search(r"epair.*sctp 192.0.2.4:.*192.0.2.3:1234", states) + assert re.search(r"epair.*sctp 192.0.2.4:.*192.0.2.2:1234", states) + + @pytest.mark.require_user("root") + def test_permutation_floating(self): + # Test that we generate all permutations of src/dst addresses. + # Assign two addresses to each end, and check for the expected states + srv_vnet = self.vnet_map["vnet2"] + + ifname = self.vnet_map["vnet1"].iface_alias_map["if1"].name + ToolsHelper.print_output("/sbin/ifconfig %s inet alias 192.0.2.4/24" % ifname) + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "block proto sctp", + "pass on lo", + "pass inet proto sctp to 192.0.2.0/24"]) + + # Sanity check, we can communicate with the primary address. + client = SCTPClient("192.0.2.3", 1234) + client.send(b"hello", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "hello" + + # Check that we have a state for 192.0.2.3 and 192.0.2.2 to 192.0.2.1, but also to 192.0.2.4 + states = ToolsHelper.get_output("/sbin/pfctl -ss") + print(states) + assert re.search(r"all sctp 192.0.2.1:.*192.0.2.3:1234", states) assert re.search(r"all sctp 192.0.2.1:.*192.0.2.2:1234", states) - assert re.search(r".*sctp 192.0.2.4:.*192.0.2.3:1234", states) + assert re.search(r"all sctp 192.0.2.4:.*192.0.2.3:1234", states) assert re.search(r"all sctp 192.0.2.4:.*192.0.2.2:1234", states) + @pytest.mark.require_user("root") + def test_limit_addresses(self): + srv_vnet = self.vnet_map["vnet2"] + + ifname = self.vnet_map["vnet1"].iface_alias_map["if1"].name + for i in range(0, 16): + ToolsHelper.print_output("/sbin/ifconfig %s inet alias 192.0.2.%d/24" % (ifname, 4 + i)) + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "block proto sctp", + "pass on lo", + "pass inet proto sctp to 192.0.2.0/24"]) + + # Set up a connection, which will try to create states for all addresses + # we have assigned + client = SCTPClient("192.0.2.3", 1234) + client.send(b"hello", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "hello" + + # But the number should be limited to 9 (original + 8 extra) + states = ToolsHelper.get_output("/sbin/pfctl -ss | grep 192.0.2.2") + print(states) + assert(states.count('\n') <= 9) + + @pytest.mark.require_user("root") + def test_disallow_related(self): + srv_vnet = self.vnet_map["vnet2"] + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "block proto sctp", + "pass inet proto sctp to 192.0.2.3", + "pass on lo"]) + + # Sanity check, we can communicate with the primary address. + client = SCTPClient("192.0.2.3", 1234) + client.send(b"hello", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "hello" + + # This shouldn't work + success=False + try: + client.newpeer("192.0.2.2") + client.send(b"world", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "world" + success=True + except: + success=False + assert not success + + # Check that we have a state for 192.0.2.3, but not 192.0.2.2 to 192.0.2.1 + states = ToolsHelper.get_output("/sbin/pfctl -ss") + assert re.search(r"all sctp 192.0.2.1:.*192.0.2.3:1234", states) + assert not re.search(r"all sctp 192.0.2.1:.*192.0.2.2:1234", states) + + @pytest.mark.require_user("root") + def test_allow_related(self): + srv_vnet = self.vnet_map["vnet2"] + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "set state-policy if-bound", + "block proto sctp", + "pass inet proto sctp to 192.0.2.3 keep state (allow-related)", + "pass on lo"]) + + # Sanity check, we can communicate with the primary address. + client = SCTPClient("192.0.2.3", 1234) + client.send(b"hello", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "hello" + + success=False + try: + client.newpeer("192.0.2.2") + client.send(b"world", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "world" + success=True + finally: + # Debug output + ToolsHelper.print_output("/sbin/pfctl -ss") + ToolsHelper.print_output("/sbin/pfctl -sr -vv") + assert success + + # Check that we have a state for 192.0.2.3 and 192.0.2.2 to 192.0.2.1 + states = ToolsHelper.get_output("/sbin/pfctl -ss") + assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.3:1234", states) + assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.2:1234", states) + class TestSCTPv6(VnetTestTemplate): REQUIRED_MODULES = ["sctp", "pf"] TOPOLOGY = { @@ -417,6 +555,7 @@ class TestSCTPv6(VnetTestTemplate): ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ "block proto sctp", + "pass on lo", "pass inet6 proto sctp to 2001:db8::0/64"]) # Sanity check, we can communicate with the primary address. @@ -454,6 +593,7 @@ class TestSCTPv6(VnetTestTemplate): ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ "block proto sctp", + "pass on lo", "pass inet6 proto sctp from 2001:db8::/64"]) # Sanity check, we can communicate with the primary address. @@ -520,7 +660,40 @@ class TestSCTPv6(VnetTestTemplate): ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ + "set state-policy if-bound", + "block proto sctp", + "pass on lo", + "pass inet6 proto sctp to 2001:db8::0/64"]) + + # Sanity check, we can communicate with the primary address. + client = SCTPClient("2001:db8::3", 1234) + client.send(b"hello", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "hello" + + # Check that we have a state for 2001:db8::3 and 2001:db8::2 to 2001:db8::1, but also to 2001:db8::4 + states = ToolsHelper.get_output("/sbin/pfctl -ss") + print(states) + assert re.search(r"epair.*sctp 2001:db8::1\[.*2001:db8::2\[1234\]", states) + assert re.search(r"epair.*sctp 2001:db8::1\[.*2001:db8::3\[1234\]", states) + assert re.search(r"epair.*sctp 2001:db8::4\[.*2001:db8::2\[1234\]", states) + assert re.search(r"epair.*sctp 2001:db8::4\[.*2001:db8::3\[1234\]", states) + + @pytest.mark.require_user("root") + def test_permutation_floating(self): + # Test that we generate all permutations of src/dst addresses. + # Assign two addresses to each end, and check for the expected states + srv_vnet = self.vnet_map["vnet2"] + + ifname = self.vnet_map["vnet1"].iface_alias_map["if1"].name + ToolsHelper.print_output("/sbin/ifconfig %s inet6 alias 2001:db8::4/64" % ifname) + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ "block proto sctp", + "pass on lo", "pass inet6 proto sctp to 2001:db8::0/64"]) # Sanity check, we can communicate with the primary address. diff --git a/tests/sys/netpfil/pf/sctp.sh b/tests/sys/netpfil/pf/sctp.sh index 66225e132d1c..57dcdad1d866 100644 --- a/tests/sys/netpfil/pf/sctp.sh +++ b/tests/sys/netpfil/pf/sctp.sh @@ -181,6 +181,64 @@ basic_v6_cleanup() pft_cleanup } +atf_test_case "reuse" "cleanup" +reuse_head() +{ + atf_set descr 'Test handling dumb clients that reuse source ports' + atf_set require.user root +} + +reuse_body() +{ + sctp_init + + j="sctp:reuse" + epair=$(vnet_mkepair) + + vnet_mkjail ${j}a ${epair}a + vnet_mkjail ${j}b ${epair}b + + jexec ${j}a ifconfig ${epair}a 192.0.2.1/24 up + jexec ${j}b ifconfig ${epair}b 192.0.2.2/24 up + # Sanity check + atf_check -s exit:0 -o ignore \ + jexec ${j}a ping -c 1 192.0.2.2 + + jexec ${j}a pfctl -e + pft_set_rules ${j}a \ + "block" \ + "pass in proto sctp to port 1234" + + echo "foo" | jexec ${j}a nc --sctp -N -l 1234 & + + # Wait for the server to start + sleep 1 + + out=$(jexec ${j}b nc --sctp -N -w 3 -p 1234 192.0.2.1 1234) + if [ "$out" != "foo" ]; then + atf_fail "SCTP connection failed" + fi + + # Now do the same thing again, with the same port numbers + jexec ${j}a pfctl -ss -v + + echo "foo" | jexec ${j}a nc --sctp -N -l 1234 & + + # Wait for the server to start + sleep 1 + + out=$(jexec ${j}b nc --sctp -N -w 3 -p 1234 192.0.2.1 1234) + if [ "$out" != "foo" ]; then + atf_fail "SCTP connection failed" + fi + jexec ${j}a pfctl -ss -v +} + +reuse_cleanup() +{ + pft_cleanup +} + atf_test_case "abort_v4" "cleanup" abort_v4_head() { @@ -504,6 +562,7 @@ pfsync_body() sctp_init pfsynct_init + vnet_init_bridge if ! kldstat -q -m carp then atf_skip "This test requires carp" @@ -655,14 +714,133 @@ pfsync_cleanup() pfsynct_cleanup } +atf_test_case "timeout" "cleanup" +timeout_head() +{ + atf_set descr 'Test setting and retrieving timeout values' + atf_set require.user root +} + +timeout_body() +{ + sctp_init + + vnet_mkjail timeout + + pft_set_rules timeout \ + "set timeout sctp.first 13" \ + "set timeout sctp.opening 14" + + atf_check -s exit:0 -o match:"sctp.first.*13" \ + jexec timeout pfctl -st + atf_check -s exit:0 -o match:"sctp.opening.*14" \ + jexec timeout pfctl -st + # We've not changed other timeouts + atf_check -s exit:0 -o match:"sctp.established.*86400" \ + jexec timeout pfctl -st +} + +timeout_cleanup() +{ + pft_cleanup +} + +atf_test_case "related_icmp" "cleanup" +related_icmp_head() +{ + atf_set descr 'Verify that ICMP messages related to an SCTP connection are allowed' + atf_set require.user root +} + +related_icmp_body() +{ + sctp_init + + epair_cl=$(vnet_mkepair) + epair_rtr=$(vnet_mkepair) + epair_srv=$(vnet_mkepair) + + ifconfig ${epair_cl}a 192.0.2.1/24 up + route add default 192.0.2.2 + + vnet_mkjail rtr ${epair_cl}b ${epair_rtr}a + jexec rtr ifconfig ${epair_cl}b 192.0.2.2/24 up + jexec rtr ifconfig ${epair_rtr}a 198.51.100.1/24 up + jexec rtr sysctl net.inet.ip.forwarding=1 + jexec rtr route add default 198.51.100.2 + + vnet_mkjail rtr2 ${epair_rtr}b ${epair_srv}a + jexec rtr2 ifconfig ${epair_rtr}b 198.51.100.2/24 up + jexec rtr2 ifconfig ${epair_srv}a 203.0.113.1/24 up + jexec rtr2 ifconfig ${epair_srv}a mtu 1300 + jexec rtr2 sysctl net.inet.ip.forwarding=1 + jexec rtr2 route add default 198.51.100.1 + + vnet_mkjail srv ${epair_srv}b + jexec srv ifconfig ${epair_srv}b 203.0.113.2/24 up + jexec srv ifconfig ${epair_srv}b mtu 1300 + jexec srv route add default 203.0.113.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping -c 1 198.51.100.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 198.51.100.2 + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.2 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "block proto icmp" \ + "pass proto sctp" + + # Make sure SCTP traffic passes + echo "foo" | jexec srv nc --sctp -N -l 1234 & + sleep 1 + + out=$(nc --sctp -N -w 3 203.0.113.2 1234) + if [ "$out" != "foo" ]; then + jexec rtr pfctl -ss -vv + jexec rtr pfctl -sr -vv + atf_fail "SCTP connection failed" + fi + + # Do we see ICMP traffic if we send overly large traffic? + echo "foo" | jexec srv nc --sctp -l 1234 >/dev/null & + sleep 1 + + atf_check -s exit:0 -o not-match:".*destination unreachable:.*" \ + netstat -s -p icmp + + # Generate traffic that will be fragmented by rtr2, and will provoke an + # ICMP unreachable - need to frag (mtu 1300) message + dd if=/dev/random bs=10000 count=1 | nc --sctp -N -w 3 203.0.113.2 1234 + + # We'd expect to see an ICMP message + atf_check -s exit:0 -o match:".*destination unreachable: [1-9]" \ + netstat -s -p icmp +} + +related_icmp_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "basic_v4" atf_add_test_case "basic_v6" + atf_add_test_case "reuse" atf_add_test_case "abort_v4" atf_add_test_case "abort_v6" atf_add_test_case "nat_v4" atf_add_test_case "nat_v6" atf_add_test_case "rdr_v4" atf_add_test_case "pfsync" + atf_add_test_case "timeout" + atf_add_test_case "related_icmp" } diff --git a/tests/sys/netpfil/pf/set_skip.sh b/tests/sys/netpfil/pf/set_skip.sh index e5b1440360e9..e984377721b8 100644 --- a/tests/sys/netpfil/pf/set_skip.sh +++ b/tests/sys/netpfil/pf/set_skip.sh @@ -26,6 +26,50 @@ . $(atf_get_srcdir)/utils.subr +atf_test_case "unset" "cleanup" +unset_head() +{ + atf_set descr 'Unset set skip test' + atf_set require.user root +} + +unset_body() +{ + pft_init + + vnet_mkjail alcatraz + jexec alcatraz ifconfig lo0 127.0.0.1/8 up + jexec alcatraz pfctl -e + pft_set_rules alcatraz "set skip on lo0" \ + "block in proto icmp" + + echo "set skip" + jexec alcatraz pfctl -v -sI + + jexec alcatraz ifconfig + atf_check -s exit:0 -o ignore jexec alcatraz ping -c 1 127.0.0.1 + + # Unset the skip on the group + pft_set_rules noflush alcatraz \ + "block in proto icmp" + + echo "No setskip" + jexec alcatraz pfctl -v -sI + + # Do flush states + jexec alcatraz pfctl -Fs + + # And now our ping is blocked + atf_check -s exit:2 -o ignore jexec alcatraz ping -c 1 127.0.0.1 + + jexec alcatraz pfctl -v -sI +} + +unset_cleanup() +{ + pft_cleanup +} + atf_test_case "set_skip_group" "cleanup" set_skip_group_head() { @@ -45,8 +89,24 @@ set_skip_group_body() pft_set_rules alcatraz "set skip on foo" \ "block in proto icmp" + echo "set skip" + jexec alcatraz pfctl -v -sI + jexec alcatraz ifconfig atf_check -s exit:0 -o ignore jexec alcatraz ping -c 1 127.0.0.1 + + # Unset the skip on the group + pft_set_rules noflush alcatraz \ + "block in proto icmp" + + # Do flush states + jexec alcatraz pfctl -Fs + + # And now our ping is blocked + atf_check -s exit:2 -o ignore jexec alcatraz ping -c 1 127.0.0.1 + + echo "No setskip" + jexec alcatraz pfctl -v -sI } set_skip_group_cleanup() @@ -163,6 +223,7 @@ pr255852_cleanup() atf_init_test_cases() { + atf_add_test_case "unset" atf_add_test_case "set_skip_group" atf_add_test_case "set_skip_group_lo" atf_add_test_case "set_skip_dynamic" diff --git a/tests/sys/netpfil/pf/set_tos.sh b/tests/sys/netpfil/pf/set_tos.sh index bfec61f0d221..75b96edbab6e 100644 --- a/tests/sys/netpfil/pf/set_tos.sh +++ b/tests/sys/netpfil/pf/set_tos.sh @@ -37,7 +37,7 @@ v4_head() atf_set require.user root # We need scapy to be installed for out test scripts to work - atf_set require.progs scapy + atf_set require.progs python3 scapy } v4_body() @@ -122,7 +122,7 @@ v6_head() atf_set require.user root # We need scapy to be installed for out test scripts to work - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() @@ -191,6 +191,22 @@ v6_body() --to 2001:db8:192::2 \ --replyif ${epair}a \ --expect-tc 0 + + # We can set tos on pass rules + pft_set_rules alcatraz "pass out set tos 13" + atf_check -s exit:0 -o ignore -e ignore ${common_dir}/pft_ping.py \ + --sendif ${epair}a \ + --to 2001:db8:192::2 \ + --replyif ${epair}a \ + --expect-tc 13 + + # And that still works with 'scrub' options too + pft_set_rules alcatraz "pass out set tos 14 scrub (min-ttl 64)" + atf_check -s exit:0 -o ignore -e ignore ${common_dir}/pft_ping.py \ + --sendif ${epair}a \ + --to 2001:db8:192::2 \ + --replyif ${epair}a \ + --expect-tc 14 } v6_cleanup() diff --git a/tests/sys/netpfil/pf/snmp.sh b/tests/sys/netpfil/pf/snmp.sh new file mode 100644 index 000000000000..37cc4b75cf92 --- /dev/null +++ b/tests/sys/netpfil/pf/snmp.sh @@ -0,0 +1,123 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Kristof Provost <kp@FreeBSD.org> +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Basic pf_snmp test' + atf_set require.user root +} + +basic_body() +{ + pft_init + + epair=$(vnet_mkepair) + + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Start bsnmpd + jexec alcatraz bsnmpd -c $(atf_get_srcdir)/bsnmpd.conf + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass" + + # Sanity check, and create state + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + # pf should be enabled + atf_check -s exit:0 -o match:'pfStatusRunning.0 = true' \ + bsnmpwalk -s public@192.0.2.1 -i pf_tree.def begemot +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_test_case "table" "cleanup" +table_head() +{ + atf_set descr 'Test tables and pf_snmp' + atf_set require.user root +} + +table_body() +{ + pft_init + + epair=$(vnet_mkepair) + + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "table <foo> counters { 192.0.2.0/24 }" \ + "pass in from <foo>" + + # Start bsnmpd after creating the table so we don't have to wait for + # a refresh timeout + jexec alcatraz bsnmpd -c $(atf_get_srcdir)/bsnmpd.conf + + # Sanity check, and create state + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + # We should have one table + atf_check -s exit:0 -o match:'pfTablesTblNumber.0 = 1' \ + bsnmpwalk -s public@192.0.2.1 -i pf_tree.def begemot + + # We have the 'foo' table + atf_check -s exit:0 -o match:'pfTablesTblDescr.* = foo' \ + bsnmpwalk -s public@192.0.2.1 -i pf_tree.def pfTables + + # Which contains address 192.0.2.0/24 + atf_check -s exit:0 -o match:'pfTablesAddrNet.* = 192.0.2.0' \ + bsnmpwalk -s public@192.0.2.1 -i pf_tree.def pfTables + atf_check -s exit:0 -o match:'pfTablesAddrPrefix.* = 24' \ + bsnmpwalk -s public@192.0.2.1 -i pf_tree.def pfTables +} + +table_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" + atf_add_test_case "table" +} diff --git a/tests/sys/netpfil/pf/src_track.sh b/tests/sys/netpfil/pf/src_track.sh index 27eb62abcf41..c24f88062c4d 100755 --- a/tests/sys/netpfil/pf/src_track.sh +++ b/tests/sys/netpfil/pf/src_track.sh @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2020 Kristof Provost <kp@FreeBSD.org> +# Copyright (c) 2024 Kajetan Staszkiewicz <vegeta@tuxpowered.net> # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -51,7 +52,19 @@ source_track_body() "pass out keep state (source-track)" ping -c 3 192.0.2.1 - jexec alcatraz pfctl -s all -v + atf_check -s exit:0 -o match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS + + # Flush all source nodes + jexec alcatraz pfctl -FS + + # We can't find the previous source node any more + atf_check -s exit:0 -o not-match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS + + # But we still have the state + atf_check -s exit:0 -o match:'all icmp 192.0.2.1:8 <- 192.0.2.2:.*' \ + jexec alcatraz pfctl -ss } source_track_cleanup() @@ -59,7 +72,444 @@ source_track_cleanup() pft_cleanup } +atf_test_case "kill" "cleanup" +kill_head() +{ + atf_set descr 'Test killing source nodes' + atf_set require.user root +} + +kill_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}b + + ifconfig ${epair}a 192.0.2.2/24 up + ifconfig ${epair}a inet alias 192.0.2.3/24 up + jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up + + # Enable pf! + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass in keep state (source-track)" \ + "pass out keep state (source-track)" + + # Establish two sources + atf_check -s exit:0 -o ignore \ + ping -c 1 -S 192.0.2.2 192.0.2.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 -S 192.0.2.3 192.0.2.1 + + # Check that both source nodes exist + atf_check -s exit:0 -o match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS + atf_check -s exit:0 -o match:'192.0.2.3 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS + + +jexec alcatraz pfctl -sS + + # Kill the 192.0.2.2 source + jexec alcatraz pfctl -K 192.0.2.2 + + # The other source still exists + atf_check -s exit:0 -o match:'192.0.2.3 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS + + # But not the one we killed + atf_check -s exit:0 -o not-match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS +} + +kill_cleanup() +{ + pft_cleanup +} + +max_src_conn_rule_head() +{ + atf_set descr 'Max connections per source per rule' + atf_set require.user root + atf_set require.progs python3 scapy +} + +max_src_conn_rule_body() +{ + setup_router_server_ipv6 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses and for tester jail + # to not respond with RST packets for SYN+ACKs. + jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2 + jexec server route add -6 2001:db8:44::0/64 2001:db8:43::1 + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b inet6 proto tcp keep state (max-src-conn 3 source-track rule overload <bad_hosts>)" \ + "pass out on ${epair_server}a inet6 proto tcp keep state" + + # Limiting of connections is done for connections which have successfully + # finished the 3-way handshake. Once the handshake is done, the state + # is moved to CLOSED state. We use pft_ping.py to check that the handshake + # was really successful and after that we check what is in pf state table. + + # 3 connections from host ::1 will be allowed. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4202 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4203 --fromaddr 2001:db8:44::1 + # The 4th connection from host ::1 will have its state killed. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4204 --fromaddr 2001:db8:44::1 + # A connection from host :2 is will be allowed. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4205 --fromaddr 2001:db8:44::2 + + states=$(mktemp) || exit 1 + jexec router pfctl -qss | normalize_pfctl_s | grep 'tcp 2001:db8:43::2\[9\] <-' > $states + + grep -qE '2001:db8:44::1\[4201\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4201 not found or not established" + grep -qE '2001:db8:44::1\[4202\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4202 not found or not established" + grep -qE '2001:db8:44::1\[4203\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4203 not found or not established" + grep -qE '2001:db8:44::2\[4205\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4205 not found or not established" + + if ( + grep -qE '2001:db8:44::1\[4204\] ' $states && + ! grep -qE '2001:db8:44::1\[4204\] CLOSED:CLOSED' $states + ); then + atf_fail "State for port 4204 found but not closed" + fi + + jexec router pfctl -T test -t bad_hosts 2001:db8:44::1 || atf_fail "Host not found in overload table" +} + +max_src_conn_rule_cleanup() +{ + pft_cleanup +} + +max_src_states_rule_head() +{ + atf_set descr 'Max states per source per rule' + atf_set require.user root + atf_set require.progs python3 scapy +} + +max_src_states_rule_body() +{ + setup_router_server_ipv6 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses and for tester jail + # to not respond with RST packets for SYN+ACKs. + jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2 + jexec server route add -6 2001:db8:44::0/64 2001:db8:43::1 + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b inet6 proto tcp from port 4210:4219 keep state (max-src-states 3 source-track rule) label rule_A" \ + "pass in on ${epair_tester}b inet6 proto tcp from port 4220:4229 keep state (max-src-states 3 source-track rule) label rule_B" \ + "pass out on ${epair_server}a keep state" + + # The option max-src-states prevents even the initial SYN packet going + # through. It's enough that we check ping_server_check_reply, no need to + # bother checking created states. + + # 2 connections from host ::1 matching rule_A will be allowed, 1 will fail to create a state. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4211 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4212 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4213 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4214 --fromaddr 2001:db8:44::1 + + # 2 connections from host ::1 matching rule_B will be allowed, 1 will fail to create a state. + # Limits from rule_A don't interfere with rule_B. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4221 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4222 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4223 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4224 --fromaddr 2001:db8:44::1 + + # 2 connections from host ::2 matching rule_B will be allowed, 1 will fail to create a state. + # Limits for host ::1 will not interfere with host ::2. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4224 --fromaddr 2001:db8:44::2 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4225 --fromaddr 2001:db8:44::2 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4226 --fromaddr 2001:db8:44::2 + ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4227 --fromaddr 2001:db8:44::2 + + # We will check the resulting source nodes, though. + # Order of source nodes in output is not guaranteed, find each one separately. + nodes=$(mktemp) || exit 1 + jexec router pfctl -qvsS | normalize_pfctl_s > $nodes + for node_regexp in \ + '2001:db8:44::1 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, filter rule 3, limit source-track$' \ + '2001:db8:44::1 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, filter rule 4, limit source-track$' \ + '2001:db8:44::2 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, filter rule 4, limit source-track$' \ + ; do + grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'" + done + + # Check if limit counters have been properly set. + jexec router pfctl -qvvsi | grep -qE 'max-src-states\s+3\s+' || atf_fail "max-src-states not set to 3" +} + +max_src_states_rule_cleanup() +{ + pft_cleanup +} + +max_src_states_global_head() +{ + atf_set descr 'Max states per source global' + atf_set require.user root + atf_set require.progs python3 scapy +} + +max_src_states_global_body() +{ + setup_router_server_ipv6 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses and for tester jail + # to not respond with RST packets for SYN+ACKs. + jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2 + jexec server route add -6 2001:db8:44::0/64 2001:db8:43::1 + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b inet6 proto tcp from port 4210:4219 keep state (max-src-states 3 source-track global) label rule_A" \ + "pass in on ${epair_tester}b inet6 proto tcp from port 4220:4229 keep state (max-src-states 3 source-track global) label rule_B" \ + "pass out on ${epair_server}a keep state" + + # Global source tracking creates a single source node shared between all + # rules for each connecting source IP address and counts states created + # by all rules. Each rule has its own max-src-conn value checked against + # that single source node. + + # 3 connections from host …::1 matching rule_A will be allowed. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4211 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4212 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4213 --fromaddr 2001:db8:44::1 + # The 4th connection matching rule_A from host …::1 will have its state killed. + ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4214 --fromaddr 2001:db8:44::1 + # A connection matching rule_B from host …::1 will have its state killed too. + ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4221 --fromaddr 2001:db8:44::1 + + nodes=$(mktemp) || exit 1 + jexec router pfctl -qvsS | normalize_pfctl_s > $nodes + cat $nodes + node_regexp='2001:db8:44::1 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, limit source-track' + grep -qE "$node_regexp" $nodes || atf_fail "Source nodes not matching expected output" +} + +max_src_states_global_cleanup() +{ + pft_cleanup +} + +sn_types_compat_head() +{ + atf_set descr 'Combination of source node types with compat NAT rules' + atf_set require.user root + atf_set require.progs python3 scapy +} + +sn_types_compat_body() +{ + setup_router_dummy_ipv6 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses. + jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2 + + # Additional gateways for route-to. + rtgw=${net_server_host_server%::*}::2:1 + jexec router ndp -s ${rtgw} 00:01:02:03:04:05 + + # This test will check for proper source node creation for: + # max-src-states -> PF_SN_LIMIT + # sticky-address -> PF_SN_NAT + # route-to -> PF_SN_ROUTE + # The test expands to all 8 combinations of those source nodes being + # present or not. + + pft_set_rules router \ + "table <rtgws> { ${rtgw} }" \ + "table <rdrgws> { 2001:db8:45::1 }" \ + "rdr on ${epair_tester}b inet6 proto tcp from 2001:db8:44::10/124 to 2001:db8:45::1 -> <rdrgws> port 4242 sticky-address" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4211 keep state label rule_3" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4212 keep state label rule_4" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4213 keep state (max-src-states 3 source-track rule) label rule_5" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4214 keep state (max-src-states 3 source-track rule) label rule_6" \ + "pass out quick on ${epair_server}a keep state" + + # We don't check if state limits are properly enforced, this is tested + # by other tests in this file. + # Source address will not match the NAT rule + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::01 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::02 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::03 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::04 --to 2001:db8:45::1 + # Source address will match the NAT rule + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::11 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::12 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::13 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::14 --to 2001:db8:45::1 + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + nodes=$(mktemp) || exit 1 + jexec router pfctl -qvvsS | normalize_pfctl_s > $nodes + + # Order of states in output is not guaranteed, find each one separately. + for state_regexp in \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::1\[4211\] .* 1:0 pkts, 76:0 bytes, rule 3$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::2\[4212\] .* 1:0 pkts, 76:0 bytes, rule 4, route sticky-address$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::3\[4213\] .* 1:0 pkts, 76:0 bytes, rule 5, limit source-track$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::4\[4214\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track, route sticky-address$' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::11\[4211\] .* 1:0 pkts, 76:0 bytes, rule 3, NAT/RDR sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::12\[4212\] .* 1:0 pkts, 76:0 bytes, rule 4, NAT/RDR sticky-address, route sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::13\[4213\] .* 1:0 pkts, 76:0 bytes, rule 5, limit source-track, NAT/RDR sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::14\[4214\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track, NAT/RDR sticky-address, route sticky-address' \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + + # Order of source nodes in output is not guaranteed, find each one separately. + for node_regexp in \ + '2001:db8:44::2 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 4, route sticky-address' \ + '2001:db8:44::3 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, limit source-track' \ + '2001:db8:44::4 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, route sticky-address' \ + '2001:db8:44::4 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \ + '2001:db8:44::11 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \ + '2001:db8:44::12 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \ + '2001:db8:44::12 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 4, route sticky-address' \ + '2001:db8:44::13 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \ + '2001:db8:44::13 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, limit source-track' \ + '2001:db8:44::14 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \ + '2001:db8:44::14 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, route sticky-address' \ + '2001:db8:44::14 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \ + ; do + grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'" + done + + ! grep -q 'filter rule 3' $nodes || atf_fail "Source node found for rule 3" +} + +sn_types_compat_cleanup() +{ + pft_cleanup +} + +sn_types_pass_head() +{ + atf_set descr 'Combination of source node types with pass NAT rules' + atf_set require.user root + atf_set require.progs python3 scapy +} + +sn_types_pass_body() +{ + setup_router_dummy_ipv6 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses. + jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2 + + # Additional gateways for route-to. + rtgw=${net_server_host_server%::*}::2:1 + jexec router ndp -s ${rtgw} 00:01:02:03:04:05 + + # This test will check for proper source node creation for: + # max-src-states -> PF_SN_LIMIT + # sticky-address -> PF_SN_NAT + # route-to -> PF_SN_ROUTE + # The test expands to all 8 combinations of those source nodes being + # present or not. + + pft_set_rules router \ + "table <rtgws> { ${rtgw} }" \ + "table <rdrgws> { 2001:db8:45::1 }" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "match in on ${epair_tester}b inet6 proto tcp from 2001:db8:44::10/124 to 2001:db8:45::1 rdr-to <rdrgws> port 4242 sticky-address label rule_3" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4211 keep state label rule_4" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4212 keep state label rule_5" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4213 keep state (max-src-states 3 source-track rule) label rule_6" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4214 keep state (max-src-states 3 source-track rule) label rule_7" \ + "pass out quick on ${epair_server}a keep state" + + # We don't check if state limits are properly enforced, this is tested + # by other tests in this file. + # Source address will not match the NAT rule + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::01 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::02 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::03 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::04 --to 2001:db8:45::1 + # Source address will match the NAT rule + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::11 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::12 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::13 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::14 --to 2001:db8:45::1 + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + nodes=$(mktemp) || exit 1 + jexec router pfctl -qvvsS | normalize_pfctl_s > $nodes + + echo " === states ===" + cat $states + echo " === nodes ===" + cat $nodes + echo " === end === " + + # Order of states in output is not guaranteed, find each one separately. + for state_regexp in \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::1\[4211\] .* 1:0 pkts, 76:0 bytes, rule 4$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::2\[4212\] .* 1:0 pkts, 76:0 bytes, rule 5, route sticky-address$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::3\[4213\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::4\[4214\] .* 1:0 pkts, 76:0 bytes, rule 7, limit source-track, route sticky-address$' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::11\[4211\] .* 1:0 pkts, 76:0 bytes, rule 4, NAT/RDR sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::12\[4212\] .* 1:0 pkts, 76:0 bytes, rule 5, NAT/RDR sticky-address, route sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::13\[4213\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track, NAT/RDR sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::14\[4214\] .* 1:0 pkts, 76:0 bytes, rule 7, limit source-track, NAT/RDR sticky-address, route sticky-address' \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + + # Order of source nodes in output is not guaranteed, find each one separately. + for node_regexp in \ + '2001:db8:44::2 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, route sticky-address' \ + '2001:db8:44::3 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \ + '2001:db8:44::4 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, route sticky-address' \ + '2001:db8:44::4 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, limit source-track' \ + '2001:db8:44::11 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \ + '2001:db8:44::12 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \ + '2001:db8:44::12 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, route sticky-address' \ + '2001:db8:44::13 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \ + '2001:db8:44::13 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \ + '2001:db8:44::14 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \ + '2001:db8:44::14 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, route sticky-address' \ + '2001:db8:44::14 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, limit source-track' \ + ; do + grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'" + done +} + +sn_types_pass_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "source_track" + atf_add_test_case "kill" + atf_add_test_case "max_src_conn_rule" + atf_add_test_case "max_src_states_rule" + atf_add_test_case "max_src_states_global" + atf_add_test_case "sn_types_compat" + atf_add_test_case "sn_types_pass" } diff --git a/tests/sys/netpfil/pf/status.sh b/tests/sys/netpfil/pf/status.sh new file mode 100644 index 000000000000..bfd916a40c01 --- /dev/null +++ b/tests/sys/netpfil/pf/status.sh @@ -0,0 +1,73 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Rubicon Communications, LLC (Netgate) +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Basic get/clear status test case' + atf_set require.user root +} + +basic_body() +{ + pft_init + + epair=$(vnet_mkepair) + + vnet_mkjail one ${epair}a + jexec one ifconfig ${epair}a 192.0.2.1/24 up + vnet_mkjail two ${epair}b + jexec two ifconfig ${epair}b 192.0.2.2/24 up + + jexec one pfctl -e + pft_set_rules one "pass" + + # Sanity check + atf_check -s exit:0 -o ignore \ + jexec two ping -c 1 192.0.2.1 + + atf_check -s exit:0 -o not-match:'searches[[:space:]]+0' \ + jexec one pfctl -si + + atf_check -s exit:0 -o ignore -e ignore \ + jexec one pfctl -Fi + + atf_check -s exit:0 -o match:'searches[[:space:]]+0' \ + jexec one pfctl -si +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" +} + diff --git a/tests/sys/netpfil/pf/syncookie.sh b/tests/sys/netpfil/pf/syncookie.sh index 8feb2816f589..fad90f3b2618 100644 --- a/tests/sys/netpfil/pf/syncookie.sh +++ b/tests/sys/netpfil/pf/syncookie.sh @@ -51,7 +51,7 @@ basic_body() vnet_mkjail alcatraz ${epair}b jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf ifconfig ${epair}a 192.0.2.2/24 up @@ -81,7 +81,7 @@ basic_body() basic_cleanup() { - rm -f inetd-alcatraz.pid + rm -f ${PWD}/inetd-alcatraz.pid pft_cleanup } @@ -100,7 +100,7 @@ basic_v6_body() vnet_mkjail alcatraz ${epair}b jexec alcatraz ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad @@ -130,7 +130,6 @@ basic_v6_body() basic_v6_cleanup() { - rm -f inetd-alcatraz.pid pft_cleanup } @@ -157,7 +156,7 @@ forward_body() jexec srv ifconfig ${epair_out}b 198.51.100.2/24 up jexec srv route add default 198.51.100.1 - jexec srv /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec srv /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf ifconfig ${epair_in}a 192.0.2.2/24 up @@ -181,7 +180,6 @@ forward_body() forward_cleanup() { - rm -f inetd-alcatraz.pid pft_cleanup } @@ -208,7 +206,7 @@ forward_v6_body() jexec srv ifconfig ${epair_out}b inet6 2001:db8:1::2/64 up no_dad jexec srv route -6 add default 2001:db8:1::1 - jexec srv /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec srv /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf ifconfig ${epair_in}a inet6 2001:db8::2/64 up no_dad @@ -232,7 +230,90 @@ forward_v6_body() forward_v6_cleanup() { - rm -f inetd-alcatraz.pid + pft_cleanup +} + +loopback_test() +{ + local addr port + + addr=$1 + port=$2 + + # syncookies don't work without state tracking enabled. + atf_check -e ignore pfctl -e + atf_check pfctl -f - <<__EOF__ +set syncookies always +pass all keep state +__EOF__ + + # Try to transmit data over a loopback connection. + cat <<__EOF__ >in +Creativity, no. +__EOF__ + nc -l $addr $port >out & + + atf_check nc -N $addr $port < in + + atf_check -o file:in cat out + + atf_check -e ignore pfctl -d +} + +atf_test_case "loopback" "cleanup" +loopback_head() +{ + atf_set descr 'Make sure that loopback v4 TCP connections work with syncookies on' + atf_set require.user root +} + +loopback_body() +{ + local epair + + pft_init + + atf_check ifconfig lo0 127.0.0.1/8 + atf_check ifconfig lo0 up + + loopback_test 127.0.0.1 8080 + + epair=$(vnet_mkepair) + atf_check ifconfig ${epair}a inet 192.0.2.1/24 + + loopback_test 192.0.2.1 8081 +} + +loopback_cleanup() +{ + pft_cleanup +} + +atf_test_case "loopback_v6" "cleanup" +loopback_v6_head() +{ + atf_set descr 'Make sure that loopback v6 TCP connections work with syncookies on' + atf_set require.user root +} + +loopback_v6_body() +{ + local epair + + pft_init + + atf_check ifconfig lo0 up + + loopback_test ::1 8080 + + epair=$(vnet_mkepair) + atf_check ifconfig ${epair}a inet6 2001:db8::1/64 + + loopback_test 2001:db8::1 8081 +} + +loopback_v6_cleanup() +{ pft_cleanup } @@ -241,7 +322,7 @@ nostate_head() { atf_set descr 'Ensure that we do not create until SYN|ACK' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } nostate_body() @@ -287,7 +368,7 @@ nostate_v6_head() { atf_set descr 'Ensure that we do not create until SYN|ACK' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } nostate_v6_body() @@ -334,7 +415,7 @@ adaptive_head() { atf_set descr 'Adaptive mode test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } adaptive_body() @@ -440,7 +521,7 @@ port_reuse_body() vnet_mkjail alcatraz ${epair}b vnet_mkjail singsing jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up - jexec alcatraz /usr/sbin/inetd -p ${HOME}/inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf ifconfig ${epair}a 192.0.2.2/24 up @@ -486,6 +567,8 @@ atf_init_test_cases() atf_add_test_case "basic_v6" atf_add_test_case "forward" atf_add_test_case "forward_v6" + atf_add_test_case "loopback" + atf_add_test_case "loopback_v6" atf_add_test_case "nostate" atf_add_test_case "nostate_v6" atf_add_test_case "adaptive" diff --git a/tests/sys/netpfil/pf/synproxy.sh b/tests/sys/netpfil/pf/synproxy.sh index 3b3dc62b8993..617fa6ba2afc 100644 --- a/tests/sys/netpfil/pf/synproxy.sh +++ b/tests/sys/netpfil/pf/synproxy.sh @@ -52,7 +52,7 @@ synproxy_body() jexec singsing ifconfig ${link}b 198.51.100.2/24 up jexec singsing route add default 198.51.100.1 - jexec singsing /usr/sbin/inetd -p inetd-singsing.pid $(atf_get_srcdir)/echo_inetd.conf + jexec singsing /usr/sbin/inetd -p ${PWD}/inetd-singsing.pid $(atf_get_srcdir)/echo_inetd.conf jexec alcatraz pfctl -e pft_set_rules alcatraz "set fail-policy return" \ @@ -74,7 +74,6 @@ synproxy_body() synproxy_cleanup() { - rm -f inetd-singsing.pid pft_cleanup } @@ -94,7 +93,7 @@ local_body() vnet_mkjail alcatraz ${epair}b jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf jexec alcatraz pfctl -e @@ -115,7 +114,6 @@ local_body() local_cleanup() { - rm -f inetd-alcatraz.pid pft_cleanup } @@ -135,7 +133,7 @@ local_v6_body() vnet_mkjail alcatraz ${epair}b jexec alcatraz ifconfig ${epair}b inet6 2001:db8:42::2/64 up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf jexec alcatraz pfctl -e @@ -155,7 +153,6 @@ local_v6_body() local_v6_cleanup() { - rm -f inetd-alcatraz.pid pft_cleanup } diff --git a/tests/sys/netpfil/pf/table.sh b/tests/sys/netpfil/pf/table.sh index 32943e659bd0..78320375db7c 100644 --- a/tests/sys/netpfil/pf/table.sh +++ b/tests/sys/netpfil/pf/table.sh @@ -109,6 +109,268 @@ v6_counters_cleanup() pft_cleanup } +atf_test_case "match_counters" "cleanup" +match_counters_head() +{ + atf_set descr 'Test that counters for tables in match rules work' + atf_set require.user root +} + +match_counters_body() +{ + pft_init + + epair_send=$(vnet_mkepair) + ifconfig ${epair_send}a 192.0.2.1/24 up + + vnet_mkjail alcatraz ${epair_send}b + jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up + jexec alcatraz pfctl -e + + pft_set_rules alcatraz \ + "table <foo> counters { 192.0.2.1 }" \ + "pass all" \ + "match in from <foo> to any" \ + "match out from any to <foo>" \ + "set skip on lo" + + atf_check -s exit:0 -o ignore ping -c 3 192.0.2.2 + + atf_check -s exit:0 -e ignore \ + -o match:'In/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'Out/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -T show -vv +} + +match_counters_cleanup() +{ + pft_cleanup +} + +atf_test_case "zero_one" "cleanup" +zero_one_head() +{ + atf_set descr 'Test zeroing a single address in a table' + atf_set require.user root +} + +pft_cleared_ctime() +{ + jexec "$1" pfctl -t "$2" -vvT show | awk -v ip="$3" ' + ($1 == ip) { m = 1 } + ($1 == "Cleared:" && m) { + sub("[[:space:]]*Cleared:[[:space:]]*", ""); print; exit }' +} + +ctime_to_unixtime() +{ + # NB: it's not TZ=UTC, it's TZ=/etc/localtime + date -jf '%a %b %d %H:%M:%S %Y' "$1" '+%s' +} + +zero_one_body() +{ + pft_init + + epair_send=$(vnet_mkepair) + ifconfig ${epair_send}a 192.0.2.1/24 up + ifconfig ${epair_send}a inet alias 192.0.2.3/24 + + vnet_mkjail alcatraz ${epair_send}b + jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up + jexec alcatraz pfctl -e + + pft_set_rules alcatraz \ + "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \ + "block all" \ + "pass in from <foo> to any" \ + "pass out from any to <foo>" \ + "set skip on lo" + + atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.1 192.0.2.2 + atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.3 192.0.2.2 + + jexec alcatraz pfctl -t foo -T show -vv + + atf_check -s exit:0 -e ignore \ + -o match:'In/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'Out/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -T show -vv + + local uniq base ts1 ts3 + uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared` + atf_check_equal 1 "$uniq" # time they were added + + base=`pft_cleared_ctime alcatraz foo 192.0.2.1` + + atf_check -s exit:0 -e ignore \ + jexec alcatraz pfctl -t foo -T zero 192.0.2.3 + + ts1=`pft_cleared_ctime alcatraz foo 192.0.2.1` + atf_check_equal "$base" "$ts1" + + ts3=`pft_cleared_ctime alcatraz foo 192.0.2.3` + atf_check test "$ts1" != "$ts3" + + ts1=`ctime_to_unixtime "$ts1"` + ts3=`ctime_to_unixtime "$ts3"` + atf_check test $(( "$ts3" - "$ts1" )) -lt 10 # (3 pings * 2) + epsilon + atf_check test "$ts1" -lt "$ts3" + + # We now have a zeroed and a non-zeroed counter, so both patterns + # should match + atf_check -s exit:0 -e ignore \ + -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -T show -vv +} + +zero_one_cleanup() +{ + pft_cleanup +} + +atf_test_case "zero_all" "cleanup" +zero_all_head() +{ + atf_set descr 'Test zeroing all table entries' + atf_set require.user root +} + +zero_all_body() +{ + pft_init + + epair_send=$(vnet_mkepair) + ifconfig ${epair_send}a 192.0.2.1/24 up + ifconfig ${epair_send}a inet alias 192.0.2.3/24 + + vnet_mkjail alcatraz ${epair_send}b + jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up + jexec alcatraz pfctl -e + + pft_set_rules alcatraz \ + "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \ + "block all" \ + "pass in from <foo> to any" \ + "pass out from any to <foo>" \ + "set skip on lo" + + atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.1 192.0.2.2 + atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.3 192.0.2.2 + + jexec alcatraz pfctl -t foo -T show -vv + atf_check -s exit:0 -e ignore \ + -o match:'In/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'Out/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -T show -vv + + atf_check -s exit:0 -e ignore \ + jexec alcatraz pfctl -t foo -T zero + + jexec alcatraz pfctl -t foo -T show -vv + atf_check -s exit:0 -e ignore \ + -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -T show -vv +} + +zero_all_cleanup() +{ + pft_cleanup +} + +atf_test_case "reset_nonzero" "cleanup" +reset_nonzero_head() +{ + atf_set descr 'Test zeroing an address with non-zero counters' + atf_set require.user root +} + +reset_nonzero_body() +{ + pft_init + + epair_send=$(vnet_mkepair) + ifconfig ${epair_send}a 192.0.2.1/24 up + ifconfig ${epair_send}a inet alias 192.0.2.3/24 + + vnet_mkjail alcatraz ${epair_send}b + jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up + jexec alcatraz pfctl -e + + pft_set_rules alcatraz \ + "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \ + "table <bar> counters { }" \ + "block all" \ + "pass in from <foo> to any" \ + "pass out from any to <foo>" \ + "pass on notReallyAnIf from <bar> to <bar>" \ + "set skip on lo" + + # Nonexisting table can't be reset, following `-T show`. + atf_check -o ignore \ + -s not-exit:0 \ + -e inline:"pfctl: Table does not exist.\n" \ + jexec alcatraz pfctl -t nonexistent -T reset + + atf_check -o ignore \ + -s exit:0 \ + -e inline:"0/0 stats cleared.\n" \ + jexec alcatraz pfctl -t bar -T reset + + # No-op is a valid operation. + atf_check -s exit:0 \ + -e inline:"0/2 stats cleared.\n" \ + jexec alcatraz pfctl -t foo -T reset + + atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.3 192.0.2.2 + + atf_check -s exit:0 -e ignore \ + -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -vvT show + + local clrd uniq + clrd=`jexec alcatraz pfctl -t foo -vvT show | grep -c Cleared` + uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared` + atf_check_equal "$clrd" 2 + atf_check_equal "$uniq" 1 # time they were added + + atf_check -s exit:0 -e ignore \ + -e inline:"1/2 stats cleared.\n" \ + jexec alcatraz pfctl -t foo -T reset + + clrd=`jexec alcatraz pfctl -t foo -vvT show | grep -c Cleared` + uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared` + atf_check_equal "$clrd" 2 + atf_check_equal "$uniq" 2 # 192.0.2.3 should get new timestamp + + atf_check -s exit:0 -e ignore \ + -o not-match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o not-match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -vvT show +} + +reset_nonzero_cleanup() +{ + pft_cleanup +} + atf_test_case "pr251414" "cleanup" pr251414_head() { @@ -324,6 +586,10 @@ atf_init_test_cases() { atf_add_test_case "v4_counters" atf_add_test_case "v6_counters" + atf_add_test_case "match_counters" + atf_add_test_case "zero_one" + atf_add_test_case "zero_all" + atf_add_test_case "reset_nonzero" atf_add_test_case "pr251414" atf_add_test_case "automatic" atf_add_test_case "network" diff --git a/tests/sys/netpfil/pf/tcp.py b/tests/sys/netpfil/pf/tcp.py new file mode 100644 index 000000000000..53e0658f419c --- /dev/null +++ b/tests/sys/netpfil/pf/tcp.py @@ -0,0 +1,158 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Rubicon Communications, LLC (Netgate) +# +# 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. + +import sys +import pytest +import random +import socket +import selectors +from utils import DelayedSend +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +class TCPClient: + def __init__(self, src, dst, sport, dport, sp): + self.src = src + self.dst = dst + self.sport = sport + self.dport = dport + self.sp = sp + self.seq = random.randrange(1, (2**32)-1) + self.ack = 0 + + def syn(self): + syn = self.sp.IP(src=self.src, dst=self.dst) \ + / self.sp.TCP(sport=self.sport, dport=self.dport, flags="S", seq=self.seq) + return syn + + def connect(self): + syn = self.syn() + r = self.sp.sr1(syn, timeout=5) + + assert r + t = r.getlayer(self.sp.TCP) + assert t + assert t.sport == self.dport + assert t.dport == self.sport + assert t.flags == "SA" + + self.seq += 1 + self.ack = t.seq + 1 + ack = self.sp.IP(src=self.src, dst=self.dst) \ + / self.sp.TCP(sport=self.sport, dport=self.dport, flags="A", ack=self.ack, seq=self.seq) + self.sp.send(ack) + + def send(self, data): + length = len(data) + pkt = self.sp.IP(src=self.src, dst=self.dst) \ + / self.sp.TCP(sport=self.sport, dport=self.dport, ack=self.ack, seq=self.seq, flags="") \ + / self.sp.Raw(data) + self.seq += length + pkt.show() + self.sp.send(pkt) + +class TestTcp(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1"]}, + "if1": {"prefixes4": [("192.0.2.1/24", "192.0.2.2/24")]}, + } + + def vnet2_handler(self, vnet): + ToolsHelper.print_output("/usr/sbin/arp -s 192.0.2.3 00:01:02:03:04:05") + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "pass" + ]) + ToolsHelper.print_output("/sbin/pfctl -x loud") + + # Start TCP listener + sel = selectors.DefaultSelector() + t = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + t.bind(("0.0.0.0", 1234)) + t.listen(100) + t.setblocking(False) + sel.register(t, selectors.EVENT_READ, data=None) + + while True: + events = sel.select(timeout=2) + for key, mask in events: + sock = key.fileobj + if key.data is None: + conn, addr = sock.accept() + print(f"Accepted connection from {addr}") + events = selectors.EVENT_READ | selectors.EVENT_WRITE + sel.register(conn, events, data="TCP") + else: + if mask & selectors.EVENT_READ: + recv_data = sock.recv(1024) + print(f"Received TCP {recv_data}") + ToolsHelper.print_output("/sbin/pfctl -ss -vv") + sock.send(recv_data) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_challenge_ack(self): + vnet = self.vnet_map["vnet1"] + ifname = vnet.iface_alias_map["if1"].name + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + a = TCPClient("192.0.2.3", "192.0.2.2", 1234, 1234, sp) + a.connect() + a.send(b"foo") + + b = TCPClient("192.0.2.3", "192.0.2.2", 1234, 1234, sp) + syn = b.syn() + syn.show() + s = DelayedSend(syn) + packets = sp.sniff(iface=ifname, timeout=3) + found = False + for p in packets: + ip = p.getlayer(sp.IP) + if not ip: + continue + tcp = p.getlayer(sp.TCP) + if not tcp: + continue + + if ip.src != "192.0.2.2": + continue + + p.show() + + assert ip.dst == "192.0.2.3" + assert tcp.sport == 1234 + assert tcp.dport == 1234 + assert tcp.flags == "A" + + # We only expect one + assert not found + found = True + + assert found diff --git a/tests/sys/netpfil/pf/tcp.sh b/tests/sys/netpfil/pf/tcp.sh index 84536480b44e..f6a9ffde1383 100644 --- a/tests/sys/netpfil/pf/tcp.sh +++ b/tests/sys/netpfil/pf/tcp.sh @@ -33,12 +33,13 @@ rst_head() { atf_set descr 'Check sequence number validation in RST packets' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } rst_body() { pft_init + vnet_init_bridge epair_srv=$(vnet_mkepair) epair_cl=$(vnet_mkepair) diff --git a/tests/sys/netpfil/pf/utils.py b/tests/sys/netpfil/pf/utils.py new file mode 100644 index 000000000000..3d1c1de86aad --- /dev/null +++ b/tests/sys/netpfil/pf/utils.py @@ -0,0 +1,46 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Rubicon Communications, LLC (Netgate) +# +# 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. +# +import threading +import time + +class DelayedSend(threading.Thread): + def __init__(self, packet, sendif=None): + threading.Thread.__init__(self) + self._packet = packet + self._sendif = sendif + + self.start() + + def run(self): + import scapy.all as sp + time.sleep(1) + + if self._sendif: + sp.sendp(self._packet, iface=self._sendif) + else: + sp.send(self._packet) + diff --git a/tests/sys/netpfil/pf/utils.subr b/tests/sys/netpfil/pf/utils.subr index 25720c1bcb66..3f8d437920f9 100644 --- a/tests/sys/netpfil/pf/utils.subr +++ b/tests/sys/netpfil/pf/utils.subr @@ -83,6 +83,8 @@ pfsynct_init() pflog_init() { + pft_init + if ! kldstat -q -m pflog; then atf_skip "This test requires pflog" fi @@ -213,8 +215,9 @@ setup_router_server_ipv4() vnet_mkjail server ${epair_server}b jexec server ifconfig ${epair_server}b ${net_server_host_server}/${net_server_mask} up jexec server route add -net ${net_tester} ${net_server_host_router} - jexec server nc -4l 666 & - sleep 1 # Give nc time to start and listen + inetd_conf=$(mktemp) + echo "discard stream tcp nowait root internal" > $inetd_conf + jexec server inetd -p ${PWD}/inetd.pid $inetd_conf } # Create a bare router jail. @@ -266,8 +269,110 @@ setup_router_server_ipv6() vnet_mkjail server ${epair_server}b jexec server ifconfig ${epair_server}b inet6 ${net_server_host_server}/${net_server_mask} up no_dad jexec server route add -6 ${net_tester} ${net_server_host_router} - jexec server nc -6l 666 & - sleep 1 # Give nc time to start and listen + inetd_conf=$(mktemp) + echo "discard stream tcp6 nowait root internal" > $inetd_conf + jexec server inetd -p ${PWD}/inetd.pid $inetd_conf +} + +# Create a router and 2 server jails for nat64 and rfc5549 test cases. +# The router is connected to servers, both are dual-stack, and to the +# tester jail. All links are dual stack. +setup_router_server_nat64() +{ + pft_init + + epair_tester=$(vnet_mkepair) + epair_server1=$(vnet_mkepair) + epair_server2=$(vnet_mkepair) + + # Funny how IPv4 address space is to small to even assign nice /24 + # prefixes on all needed networks. On IPv6 we have a separate /64 for + # each link, loopback server, and client/SNAT pool. On IPv4 we must + # use small /28 prefixes, so even though we define all networks + # as variables we can't easily use them in tests if additional addresses + # are needed. + + # IP addresses which can be used by the tester jail. + # Can be used as SNAT or as source with pft_ping.py. It is up to + # the test code to make them accessible from router. + net_clients_4=203.0.113 + net_clients_4_mask=24 + net_clients_6=2001:db8:44 + net_clients_6_mask=64 + + # IP addresses on loopback interfaces of both servers. They can be + # accessed using the route-to targtet. + host_server_4=192.0.2.100 + host_server_6=2001:db8:4203::100 + + net_tester_4=198.51.100 + net_tester_4_mask=28 + net_tester_4_host_router=198.51.100.1 + net_tester_4_host_tester=198.51.100.2 + + net_tester_6=2001:db8:4200 + net_tester_6_mask=64 + net_tester_6_host_router=2001:db8:4200::1 + net_tester_6_host_tester=2001:db8:4200::2 + + net_server1_4=198.51.100 + net_server1_4_mask=28 + net_server1_4_host_router=198.51.100.17 + net_server1_4_host_server=198.51.100.18 + + net_server1_6=2001:db8:4201 + net_server1_6_mask=64 + net_server1_6_host_router=2001:db8:4201::1 + net_server1_6_host_server=2001:db8:4201::2 + + net_server2_4=198.51.100 + net_server2_4_mask=28 + net_server2_4_host_router=198.51.100.33 + net_server2_4_host_server=198.51.100.34 + + net_server2_6=2001:db8:4202 + net_server2_6_mask=64 + net_server2_6_host_router=2001:db8:4202::1 + net_server2_6_host_server=2001:db8:4202::2 + + vnet_mkjail router ${epair_tester}b ${epair_server1}a ${epair_server2}a + jexec router ifconfig ${epair_tester}b inet ${net_tester_4_host_router}/${net_tester_4_mask} up + jexec router ifconfig ${epair_tester}b inet6 ${net_tester_6_host_router}/${net_tester_6_mask} up no_dad + jexec router ifconfig ${epair_server1}a inet ${net_server1_4_host_router}/${net_server1_4_mask} up + jexec router ifconfig ${epair_server1}a inet6 ${net_server1_6_host_router}/${net_server1_6_mask} up no_dad + jexec router ifconfig ${epair_server2}a inet ${net_server2_4_host_router}/${net_server2_4_mask} up + jexec router ifconfig ${epair_server2}a inet6 ${net_server2_6_host_router}/${net_server2_6_mask} up no_dad + jexec router sysctl net.inet.ip.forwarding=1 + jexec router sysctl net.inet6.ip6.forwarding=1 + jexec router pfctl -e + + ifconfig ${epair_tester}a inet ${net_tester_4_host_tester}/${net_tester_4_mask} up + ifconfig ${epair_tester}a inet6 ${net_tester_6_host_tester}/${net_tester_6_mask} up no_dad + route add 0.0.0.0/0 ${net_tester_4_host_router} + route add -6 ::/0 ${net_tester_6_host_router} + + inetd_conf=$(mktemp) + echo "discard stream tcp46 nowait root internal" >> $inetd_conf + + vnet_mkjail server1 ${epair_server1}b + jexec server1 /etc/rc.d/netif start lo0 + jexec server1 ifconfig ${epair_server1}b inet ${net_server1_4_host_server}/${net_server1_4_mask} up + jexec server1 ifconfig ${epair_server1}b inet6 ${net_server1_6_host_server}/${net_server1_6_mask} up no_dad + jexec server1 ifconfig lo0 ${host_server_4}/32 alias + jexec server1 ifconfig lo0 inet6 ${host_server_6}/128 alias + jexec server1 inetd -p ${PWD}/inetd_1.pid $inetd_conf + jexec server1 route add 0.0.0.0/0 ${net_server1_4_host_router} + + jexec server1 route add -6 ::/0 ${net_server1_6_host_router} + vnet_mkjail server2 ${epair_server2}b + jexec server2 /etc/rc.d/netif start lo0 + jexec server2 ifconfig ${epair_server2}b inet ${net_server2_4_host_server}/${net_server2_4_mask} up + jexec server2 ifconfig ${epair_server2}b inet6 ${net_server2_6_host_server}/${net_server2_6_mask} up no_dad + jexec server2 ifconfig lo0 ${host_server_4}/32 alias + jexec server2 ifconfig lo0 inet6 ${host_server_6}/128 alias + jexec server2 inetd -p ${PWD}/inetd_2.pid $inetd_conf + jexec server2 route add 0.0.0.0/0 ${net_server2_4_host_router} + jexec server2 route add -6 ::/0 ${net_server2_6_host_router} } # Ping the dummy static NDP target. @@ -297,3 +402,13 @@ ping_server_check_reply() --replyif ${epair_tester}a \ $params } + +normalize_pfctl_s() +{ + # `pfctl -s[rsS]` output is divided into sections. Each rule, state or + # source node starts with the beginning of a line and next lines with leading + # spaces are various parameters of said rule, state or source node. + # Convert it into a single line per entry, and remove multiple spaces, + # so that regular expressions for matching them in tests can be simpler. + awk '{ if ($0 ~ /^[^ ]/ && NR > 1) print(""); gsub(/ +/, " ", $0); printf("%s", $0); } END {print("");}' +} |