aboutsummaryrefslogtreecommitdiff
path: root/tests/sys/netpfil
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys/netpfil')
-rw-r--r--tests/sys/netpfil/Makefile1
-rw-r--r--tests/sys/netpfil/common/Makefile9
-rw-r--r--tests/sys/netpfil/common/divapp.c (renamed from tests/sys/netpfil/pf/divapp.c)15
-rw-r--r--tests/sys/netpfil/common/dummynet.sh20
-rw-r--r--tests/sys/netpfil/common/forward.sh2
-rw-r--r--tests/sys/netpfil/common/pft_ping.py384
-rw-r--r--tests/sys/netpfil/common/rdr.sh1
-rw-r--r--tests/sys/netpfil/common/sniffer.py13
-rw-r--r--tests/sys/netpfil/common/tos.sh2
-rw-r--r--tests/sys/netpfil/ipfw/Makefile8
-rw-r--r--tests/sys/netpfil/ipfw/divert.sh281
-rw-r--r--tests/sys/netpfil/pf/Makefile39
-rw-r--r--tests/sys/netpfil/pf/altq.sh6
-rw-r--r--tests/sys/netpfil/pf/anchor.sh337
-rw-r--r--tests/sys/netpfil/pf/bsnmpd.conf47
-rw-r--r--tests/sys/netpfil/pf/debug.sh106
-rw-r--r--tests/sys/netpfil/pf/divert-to.sh227
-rw-r--r--tests/sys/netpfil/pf/dup.sh2
-rw-r--r--tests/sys/netpfil/pf/ether.sh18
-rw-r--r--tests/sys/netpfil/pf/forward.sh4
-rw-r--r--tests/sys/netpfil/pf/frag-adjhole.py58
-rw-r--r--tests/sys/netpfil/pf/frag-overhole.py83
-rw-r--r--tests/sys/netpfil/pf/frag6.py248
-rw-r--r--tests/sys/netpfil/pf/fragmentation_compat.sh55
-rw-r--r--tests/sys/netpfil/pf/fragmentation_no_reassembly.sh6
-rw-r--r--tests/sys/netpfil/pf/fragmentation_pass.sh217
-rw-r--r--tests/sys/netpfil/pf/get_state.sh2
-rw-r--r--tests/sys/netpfil/pf/header.py216
-rw-r--r--tests/sys/netpfil/pf/icmp.py254
-rw-r--r--tests/sys/netpfil/pf/icmp.sh69
-rw-r--r--tests/sys/netpfil/pf/icmp6.sh204
-rw-r--r--tests/sys/netpfil/pf/if_enc.sh178
-rw-r--r--tests/sys/netpfil/pf/ioctl/Makefile1
-rw-r--r--tests/sys/netpfil/pf/ioctl/validation.c35
-rw-r--r--tests/sys/netpfil/pf/killstate.sh75
-rw-r--r--tests/sys/netpfil/pf/limits.sh119
-rw-r--r--tests/sys/netpfil/pf/map_e.sh91
-rw-r--r--tests/sys/netpfil/pf/match.sh112
-rw-r--r--tests/sys/netpfil/pf/max_pkt_rate.sh121
-rw-r--r--tests/sys/netpfil/pf/max_pkt_size.sh122
-rwxr-xr-xtests/sys/netpfil/pf/max_states.sh62
-rw-r--r--tests/sys/netpfil/pf/mbuf.sh236
-rw-r--r--tests/sys/netpfil/pf/modulate.sh4
-rw-r--r--tests/sys/netpfil/pf/nat.sh709
-rw-r--r--tests/sys/netpfil/pf/nat64.py274
-rw-r--r--tests/sys/netpfil/pf/nat64.sh1056
-rw-r--r--tests/sys/netpfil/pf/nat66.py17
-rw-r--r--tests/sys/netpfil/pf/pass_block.sh164
-rw-r--r--tests/sys/netpfil/pf/pflog.sh323
-rw-r--r--tests/sys/netpfil/pf/pflow.sh8
-rw-r--r--tests/sys/netpfil/pf/pfsync.sh338
-rw-r--r--tests/sys/netpfil/pf/pft_read_ipfix.py2
-rw-r--r--tests/sys/netpfil/pf/proxy.sh4
-rw-r--r--tests/sys/netpfil/pf/rdr-srcport.py20
-rw-r--r--tests/sys/netpfil/pf/rdr.sh193
-rw-r--r--tests/sys/netpfil/pf/return.py153
-rw-r--r--tests/sys/netpfil/pf/ridentifier.sh16
-rw-r--r--tests/sys/netpfil/pf/route_to.sh197
-rw-r--r--tests/sys/netpfil/pf/rtable.sh4
-rw-r--r--tests/sys/netpfil/pf/rules_counter.sh49
-rw-r--r--tests/sys/netpfil/pf/scrub.sh16
-rw-r--r--tests/sys/netpfil/pf/scrub_compat.sh16
-rw-r--r--tests/sys/netpfil/pf/scrub_pass.sh12
-rw-r--r--tests/sys/netpfil/pf/sctp.py181
-rw-r--r--tests/sys/netpfil/pf/sctp.sh178
-rw-r--r--tests/sys/netpfil/pf/set_skip.sh61
-rw-r--r--tests/sys/netpfil/pf/set_tos.sh20
-rw-r--r--tests/sys/netpfil/pf/snmp.sh123
-rwxr-xr-xtests/sys/netpfil/pf/src_track.sh452
-rw-r--r--tests/sys/netpfil/pf/status.sh73
-rw-r--r--tests/sys/netpfil/pf/syncookie.sh107
-rw-r--r--tests/sys/netpfil/pf/synproxy.sh9
-rw-r--r--tests/sys/netpfil/pf/table.sh266
-rw-r--r--tests/sys/netpfil/pf/tcp.py158
-rw-r--r--tests/sys/netpfil/pf/tcp.sh3
-rw-r--r--tests/sys/netpfil/pf/utils.py46
-rw-r--r--tests/sys/netpfil/pf/utils.subr123
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("");}'
+}