git: 5d1219378dd5 - main - pf: teach nat64 to handle 0 UDP checksums

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Tue, 17 Dec 2024 10:08:20 UTC
The branch main has been updated by kp:

URL: https://cgit.FreeBSD.org/src/commit/?id=5d1219378dd5d9b031926cf7806455f33677792b

commit 5d1219378dd5d9b031926cf7806455f33677792b
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2024-12-16 10:23:59 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2024-12-17 10:07:19 +0000

    pf: teach nat64 to handle 0 UDP checksums
    
    For IPv4 it's valid for a UDP checksum to be 0 (i.e. no checksum). This isn't
    the case for IPv6, so if we translate a UDP packet from IPv4 to IPv6 we need to
    ensure that the checksum is calculated.
    
    Add a test case to verify this. Rework the server jail so it can listen for TCP
    and UDP packets at the same time.
    
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
---
 sys/netpfil/pf/pf.c           | 12 +++++++++
 tests/sys/netpfil/pf/nat64.py | 61 ++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 72 insertions(+), 1 deletion(-)

diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index 9128562fd71c..f2e19693b863 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -3469,6 +3469,7 @@ pf_translate_af(struct pf_pdesc *pd)
 		ip4->ip_dst = pd->ndaddr.v4;
 		pd->src = (struct pf_addr *)&ip4->ip_src;
 		pd->dst = (struct pf_addr *)&ip4->ip_dst;
+		pd->off = sizeof(struct ip);
 		break;
 	case AF_INET6:
 		ip6 = mtod(pd->m, struct ip6_hdr *);
@@ -3485,6 +3486,7 @@ pf_translate_af(struct pf_pdesc *pd)
 		ip6->ip6_dst = pd->ndaddr.v6;
 		pd->src = (struct pf_addr *)&ip6->ip6_src;
 		pd->dst = (struct pf_addr *)&ip6->ip6_dst;
+		pd->off = sizeof(struct ip6_hdr);
 
 		/*
 		 * If we're dealing with a reassembled packet we need to adjust
@@ -9094,6 +9096,16 @@ pf_route6(struct mbuf **m, struct pf_krule *r, struct ifnet *oifp,
 		PF_STATE_UNLOCK(s);
 	}
 
+	if (pd->af != pd->naf) {
+		struct udphdr *uh = &pd->hdr.udp;
+
+		if (pd->proto == IPPROTO_UDP && uh->uh_sum == 0) {
+			uh->uh_sum = in6_cksum_pseudo(ip6,
+			    ntohs(uh->uh_ulen), IPPROTO_UDP, 0);
+			m_copyback(m0, pd->off, sizeof(*uh), pd->hdr.any);
+		}
+	}
+
 	if (ifp == NULL) {
 		m0 = *m;
 		*m = NULL;
diff --git a/tests/sys/netpfil/pf/nat64.py b/tests/sys/netpfil/pf/nat64.py
index eeddd5118168..64ec5ae15262 100644
--- a/tests/sys/netpfil/pf/nat64.py
+++ b/tests/sys/netpfil/pf/nat64.py
@@ -25,6 +25,9 @@
 # SUCH DAMAGE.
 
 import pytest
+import selectors
+import socket
+import sys
 from atf_python.sys.net.tools import ToolsHelper
 from atf_python.sys.net.vnet import VnetTestTemplate
 
@@ -41,7 +44,44 @@ class TestNAT64(VnetTestTemplate):
     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("echo foo | nc -l 1234 &")
+        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
@@ -130,3 +170,22 @@ class TestNAT64(VnetTestTemplate):
         # Check the hop limit
         ip6 = reply.getlayer(sp.IPv6)
         assert ip6.hlim == 62
+
+    @pytest.mark.require_user("root")
+    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