diff --git a/tests/sys/netpfil/pf/nat64.py b/tests/sys/netpfil/pf/nat64.py index 0908c90c34a0..705de72f5bc4 100644 --- a/tests/sys/netpfil/pf/nat64.py +++ b/tests/sys/netpfil/pf/nat64.py @@ -1,356 +1,358 @@ # # 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", "pflog" ] 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/sysctl net.inet6.ip6.forwarding=1") 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]) + "block", + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }", + "pass in on %s inet6 af-to inet from 192.0.2.1" % ifname, + ]) vnet.pipe.send(socket.if_nametoindex("pflog0")) @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 @pytest.mark.require_user("root") @pytest.mark.require_progs(["scapy"]) def test_bad_len(self): """ PR 288224: we can panic if the IPv6 plen is longer than the packet length. """ 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, plen=512) \ / sp.ICMPv6EchoRequest() / sp.Raw("foo") reply = sp.sr1(packet, timeout=3) # We don't expect a reply to a corrupted packet assert not reply @pytest.mark.require_user("root") @pytest.mark.require_progs(["scapy"]) def test_noip6(self): """ PR 288263: link-local target address in icmp6 ADVERT can cause NULL deref """ ifname = self.vnet.iface_alias_map["if1"].name gw_mac = self.vnet.iface_alias_map["if1"].epairb.ether scopeid = self.wait_object(self.vnet_map["vnet2"].pipe) ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") import scapy.all as sp pkt = sp.Ether(dst=gw_mac) \ / sp.IPv6(dst="64:ff9b::203.0.113.2") \ / sp.ICMPv6ND_NA(tgt="FFA2:%x:2821:125F:1D27:B3B2:3F6F:C43C" % scopeid) pkt.show() sp.hexdump(pkt) s = DelayedSend(pkt, sendif=ifname) packets = sp.sniff(iface=ifname, timeout=5) for r in packets: r.show() # Try scope id that likely doesn't have an interface at all pkt = sp.Ether(dst=gw_mac) \ / sp.IPv6(dst="64:ff9b::203.0.113.2") \ / sp.ICMPv6ND_NA(tgt="FFA2:%x:2821:125F:1D27:B3B2:3F6F:C43C" % 255) pkt.show() sp.hexdump(pkt) s = DelayedSend(pkt, sendif=ifname) packets = sp.sniff(iface=ifname, timeout=5) for r in packets: r.show() @pytest.mark.require_user("root") @pytest.mark.require_progs(["scapy"]) def test_ttl_zero(self): """ PR 288274: we can use an mbuf after free on TTL = 0 """ ifname = self.vnet.iface_alias_map["if1"].name gw_mac = self.vnet.iface_alias_map["if1"].epairb.ether ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") import scapy.all as sp pkt = sp.Ether(dst=gw_mac) \ / sp.IPv6(dst="64:ff9b::192.0.2.2", hlim=0) \ / sp.SCTP(sport=1111, dport=2222) \ / sp.SCTPChunkInit(init_tag=1, n_in_streams=1, n_out_streams=1, \ a_rwnd=1500, params=[ \ sp.SCTPChunkParamIPv4Addr() \ ]) pkt.show() sp.hexdump(pkt) s = DelayedSend(pkt, sendif=ifname) packets = sp.sniff(iface=ifname, timeout=5) for r in packets: r.show() diff --git a/tests/sys/netpfil/pf/nat64.sh b/tests/sys/netpfil/pf/nat64.sh index f92a69f2abce..4438ad6abb85 100644 --- a/tests/sys/netpfil/pf/nat64.sh +++ b/tests/sys/netpfil/pf/nat64.sh @@ -1,1065 +1,1195 @@ # # 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() { + state_policy="${1:-if-bound}" nat64_setup_base pft_set_rules rtr \ "set reassemble yes" \ - "set state-policy if-bound" \ + "set state-policy ${state_policy}" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" } nat64_setup_out() { + state_policy="${1:-if-bound}" 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" + "set state-policy ${state_policy}" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair}b from any to 64:ff9b::/96" \ + "pass out on ${epair_link}a from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" } 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_test_case "tcp_in_if_bound" "cleanup" +tcp_in_if_bound_head() { - atf_set descr 'TCP NAT64 test on inbound interface' + atf_set descr 'TCP NAT64 test on inbound interface, if-bound states' atf_set require.user root } -tcp_in_body() +tcp_in_if_bound_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 3 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 + + # Interfaces of the state are reversed when doing inbound NAT64! + # FIXME: Packets counters seem wrong! + states=$(mktemp) || exit 1 + jexec rtr pfctl -qvvss | normalize_pfctl_s > $states + for state_regexp in \ + "${epair_link}a tcp 192.0.2.1:[0-9]+ \(2001:db8::2\[[0-9]+\]\) -> 192.0.2.2:1234 \(64:ff9b::c000:202\[1234\]\) .* 9:9 pkts.* rule 3 .* origif: ${epair}b" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + [ $(cat $states | grep tcp | wc -l) -eq 1 ] || atf_fail "Not exactly 1 state found!" } -tcp_in_cleanup() +tcp_in_if_bound_cleanup() { pft_cleanup } -atf_test_case "tcp_out" "cleanup" -tcp_out_head() +atf_test_case "tcp_out_if_bound" "cleanup" +tcp_out_if_bound_head() { - atf_set descr 'TCP NAT64 test on outbound interface' + atf_set descr 'TCP NAT64 test on outbound interface, if-bound states' atf_set require.user root } -tcp_out_body() +tcp_out_if_bound_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 3 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 + + # Origif is not printed when identical as if. + states=$(mktemp) || exit 1 + jexec rtr pfctl -qvvss | normalize_pfctl_s > $states + for state_regexp in \ + "${epair}b tcp 64:ff9b::c000:202\[1234\] <- 2001:db8::2\[[0-9]+\] .* 5:4 pkts.* rule 3 .*creatorid" \ + "${epair_link}a tcp 192.0.2.1:[0-9]+ \(64:ff9b::c000:202\[1234\]\) -> 192.0.2.2:1234 \(2001:db8::2\[[0-9]+\]\).* 5:4 pkts.* rule 4 .*creatorid" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + [ $(cat $states | grep tcp | wc -l) -eq 2 ] || atf_fail "Not exactly 2 states found!" } -tcp_out_cleanup() +tcp_out_if_bound_cleanup() +{ + pft_cleanup +} + +atf_test_case "tcp_in_floating" "cleanup" +tcp_in_floating_head() +{ + atf_set descr 'TCP NAT64 test on inbound interface, floating states' + atf_set require.user root +} + +tcp_in_floating_body() +{ + nat64_setup_in "floating" + + echo "foo" | jexec dst nc -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 3 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 + + # Interfaces of the state are reversed when doing inbound NAT64! + # FIXME: Packets counters seem wrong! + states=$(mktemp) || exit 1 + jexec rtr pfctl -qvvss | normalize_pfctl_s > $states + for state_regexp in \ + "all tcp 192.0.2.1:[0-9]+ \(2001:db8::2\[[0-9]+\]\) -> 192.0.2.2:1234 \(64:ff9b::c000:202\[1234\]\).* 9:9 pkts.* rule 3 .* origif: ${epair}b" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + [ $(cat $states | grep tcp | wc -l) -eq 1 ] || atf_fail "Not exactly 1 state found!" +} + +tcp_in_floating_cleanup() +{ + pft_cleanup +} + +atf_test_case "tcp_out_floating" "cleanup" +tcp_out_floating_head() +{ + atf_set descr 'TCP NAT64 test on outbound interface, floating states' + atf_set require.user root +} + +tcp_out_floating_body() +{ + nat64_setup_out "floating" + + echo "foo" | jexec dst nc -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 3 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 + + # Origif is not printed when identical as if. + states=$(mktemp) || exit 1 + jexec rtr pfctl -qvvss | normalize_pfctl_s > $states + for state_regexp in \ + "all tcp 64:ff9b::c000:202\[1234\] <- 2001:db8::2\[[0-9]+\] .* 5:4 pkts,.* rule 3 .*creatorid"\ + "all tcp 192.0.2.1:[0-9]+ \(64:ff9b::c000:202\[1234\]\) -> 192.0.2.2:1234 \(2001:db8::2\[[0-9]+\]\) .* 5:4 pkts,.* rule 4 .*creatorid"\ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + [ $(cat $states | grep tcp | wc -l) -eq 2 ] || atf_fail "Not exactly 2 states found!" +} + +tcp_out_floating_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 3 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 3 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 3 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 3 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)" + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "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" + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "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" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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 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 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 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 { 192.0.2.2/31 }" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from 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 { 192.0.2.1, 192.0.2.3, 192.0.2.4 }" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from ${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" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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 states=$(mktemp) || exit 1 jexec rtr pfctl -qvvss | normalize_pfctl_s > $states for state_regexp in \ "${epair}b ipv6-icmp 192.0.2.1:.* \(2001:db8::2\[[0-9]+\]\) -> 192.0.2.2:8 \(64:ff9b::c000:202\[[0-9]+\]\).*4:2 pkts.*route-to: 192.0.2.2@${epair_link}a" \ ; do grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" done } 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" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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 "tcp_in_if_bound" + atf_add_test_case "tcp_out_if_bound" + atf_add_test_case "tcp_in_floating" + atf_add_test_case "tcp_out_floating" 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/src_track.sh b/tests/sys/netpfil/pf/src_track.sh index 68e7e72f5018..ae60a5df809b 100755 --- a/tests/sys/netpfil/pf/src_track.sh +++ b/tests/sys/netpfil/pf/src_track.sh @@ -1,577 +1,579 @@ # # SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2020 Kristof Provost # Copyright (c) 2024 Kajetan Staszkiewicz # # 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 "source_track" "cleanup" source_track_head() { atf_set descr 'Basic source tracking test' atf_set require.user root } source_track_body() { pft_init epair=$(vnet_mkepair) vnet_mkjail alcatraz ${epair}b ifconfig ${epair}a 192.0.2.2/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)" ping -c 3 192.0.2.1 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() { 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 )" \ "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 { ${rtgw} }" \ "table { 2001:db8:45::1 }" \ "rdr on ${epair_tester}b inet6 proto tcp from 2001:db8:44::10/124 to 2001:db8:45::1 -> 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 ) inet6 proto tcp from port 4211 keep state label rule_3" \ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a ) 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 ) 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 ) 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 { ${rtgw} }" \ "table { 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 port 4242 sticky-address label rule_3" \ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a ) inet6 proto tcp from port 4211 keep state label rule_4" \ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a ) 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 ) 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 ) 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_test_case "mixed_af" "cleanup" mixed_af_head() { atf_set descr 'Test mixed address family source tracking' atf_set require.user root atf_set require.progs python3 scapy } mixed_af_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 pfctl -e pft_set_rules router \ "set reassemble yes" \ "set state-policy if-bound" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "pass in on ${epair_tester}b \ route-to { (${epair_server1}a ${net_server1_4_host_server}) \ } sticky-address \ inet6 proto tcp from any to 64:ff9b::/96 \ af-to inet from ${net_clients_4}.0/${net_clients_4_mask} round-robin sticky-address" atf_check -s exit:0 ${common_dir}/pft_ping.py \ --sendif ${epair_tester}a \ --replyif ${epair_tester}a \ --fromaddr 2001:db8:44::1 \ --to 64:ff9b::192.0.2.100 \ --ping-type=tcp3way \ --send-sport=4201 states=$(mktemp) || exit 1 jexec router pfctl -qvvss | normalize_pfctl_s > $states nodes=$(mktemp) || exit 1 jexec router pfctl -qvvsS | normalize_pfctl_s > $nodes # States are checked for proper route-to information. # The route-to gateway is IPv4. for state_regexp in \ "${epair_tester}b tcp 203.0.113.0:4201 \(2001:db8:44::1\[4201\]\) -> 192.0.2.100:9 \(64:ff9b::c000:264\[9\]\) .* route-to: 198.51.100.18@${epair_server1}a" \ ; do grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" done # Source nodes map IPv6 source address onto IPv4 gateway and IPv4 SNAT address. for node_regexp in \ '2001:db8:44::1 -> 203.0.113.0 .* states 1, .* NAT/RDR sticky-address' \ '2001:db8:44::1 -> 198.51.100.18 .* states 1, .* route sticky-address' \ ; do grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'" done } mixed_af_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" atf_add_test_case "mixed_af" }