Index: files.npf
===================================================================
RCS file: /cvsroot/src/sys/net/npf/files.npf,v
retrieving revision 1.24
diff -p -u -r1.24 files.npf
--- files.npf	1 Jun 2025 00:24:19 -0000	1.24
+++ files.npf	27 Mar 2026 14:13:42 -0000
@@ -44,6 +44,7 @@ file	net/npf/lpm.c				npf
 file	net/npf/npf_ext_log.c			npf
 file	net/npf/npf_ext_normalize.c		npf
 file	net/npf/npf_ext_rndblock.c		npf
+file	net/npf/npf_ext_route.c			npf
 
 # ALGs
 file	net/npf/npf_alg_icmp.c			npf
Index: npf_ext_route.c
===================================================================
RCS file: npf_ext_route.c
diff -N npf_ext_route.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ npf_ext_route.c	27 Mar 2026 14:13:42 -0000
@@ -0,0 +1,353 @@
+/*	$NetBSD$	*/
+
+/*-
+ * Copyright (c) 2013 The NetBSD Foundation, Inc.
+ * All rights reserved.
+ *
+ * This code is derived from software contributed to The NetBSD Foundation
+ * by Mindaugas Rasiukevicius.
+ *
+ * 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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.
+ */
+
+/*
+ * NPF route extension.
+ */
+
+#include <sys/cdefs.h>
+__KERNEL_RCSID(0, "$NetBSD$");
+
+#include <sys/types.h>
+#include <sys/module.h>
+
+#include <sys/conf.h>
+#include <sys/kmem.h>
+#include <sys/mbuf.h>
+#include <sys/mutex.h>
+#include <sys/queue.h>
+
+#include <net/if.h>
+#include <net/if_types.h>
+#include <net/bpf.h>
+
+#include <netinet/in.h>
+#include <netinet/in_offload.h>
+#include <netinet6/in6_offload.h>
+#include <netinet6/scope6_var.h>
+
+#include "npf_impl.h"
+
+NPF_EXT_MODULE(npf_ext_route, "");
+
+#define	NPFEXT_ROUTE_VER		1
+
+static void *		npf_ext_route_id;
+
+typedef struct {
+	char *if_name;
+	bool duplicate;
+	char *gateway;
+	char *gateway6;
+	struct sockaddr_storage nexthop;
+	struct sockaddr_storage nexthop6;
+} npf_ext_route_t;
+
+static int
+parse_address(const char *s, struct sockaddr_storage *res)
+{
+	static const char hex[] = "0123456789abcdef";
+	size_t n, i, k;
+	uint8_t b, *addr;
+	const char *p;
+
+	memset(res, 0, sizeof(*res));
+
+	if (s == NULL)
+		return 0;
+
+	n = strlen(s);
+	switch (n) {
+	case 8:
+		res->ss_family = AF_INET;
+		res->ss_len = sizeof(struct sockaddr_in);
+		addr = (uint8_t*)&(satosin(sstosa(res))->sin_addr.s_addr);
+		break;
+#ifdef INET6
+	case 32:
+		res->ss_family = AF_INET6;
+		res->ss_len = sizeof(struct sockaddr_in6);
+		addr = &(satosin6(sstosa(res))->sin6_addr.s6_addr8[0]);
+		break;
+#endif
+	default:
+		return 0;
+	}
+
+	for (k=0, i=1; i<n; ++k, i+=2) {
+		p = strchr(hex, s[i-1]);
+		if (p == NULL)
+			return 0;
+		b = (p - hex);
+		p = strchr(hex, s[i]);
+		if (p == NULL)
+			return 0;
+		b = b << 4 | (p - hex);
+		addr[k] = b;
+	}
+
+	return 1;
+}
+
+static npf_ext_route_t *the_route;
+
+static int
+npf_route_ctor(npf_rproc_t *rp, const nvlist_t *params)
+{
+	npf_ext_route_t *route;
+	const char *s;
+	bool b;
+
+	route = kmem_zalloc(sizeof(npf_ext_route_t), KM_SLEEP);
+
+	s = dnvlist_get_string(params, "iface", NULL);
+	if (s)
+		route->if_name = kmem_strdup(s, KM_SLEEP);
+
+	b = dnvlist_get_bool(params, "dup", false);
+	route->duplicate = b;
+
+	s = dnvlist_get_string(params, "via", NULL);
+	if (parse_address(s, &route->nexthop))
+		route->gateway = kmem_strdup(s, KM_SLEEP);
+
+	s = dnvlist_get_string(params, "via6", NULL);
+	if (parse_address(s, &route->nexthop6))
+		route->gateway6 = kmem_strdup(s, KM_SLEEP);
+
+the_route = route;
+
+	npf_rproc_assign(rp, route);
+	return 0;
+}
+
+static void
+npf_route_dtor(npf_rproc_t *rp, void *meta)
+{
+	npf_ext_route_t *route = meta;
+
+	if (route->gateway6)
+		kmem_strfree(route->gateway6);
+	if (route->gateway)
+		kmem_strfree(route->gateway);
+	if (route->if_name)
+		kmem_strfree(route->if_name);
+	kmem_free(route, sizeof(npf_ext_route_t));
+
+if (route == the_route)
+	the_route = NULL;
+}
+
+static void
+npf_route_nexthop(npf_cache_t *npc,
+    struct sockaddr_storage *ssp, int family, int socklen)
+{
+	ssp->ss_family = family;
+	ssp->ss_len = socklen;
+
+	switch (ssp->ss_family) {
+	case AF_INET:
+		memcpy(&(satosin(sstosa(ssp))->sin_addr.s_addr),
+		    npc->npc_ips[NPF_DST],
+		    npc->npc_alen);
+		break;
+	case AF_INET6:
+		memcpy(&(satosin6(sstosa(ssp))->sin6_addr.s6_addr8[0]),
+		    npc->npc_ips[NPF_DST],
+		    npc->npc_alen);
+		break;
+	}
+}
+
+static void
+npf_route_checksums(int family, struct mbuf *m)
+{
+	struct ip *ip;
+
+	switch (family) {
+	case AF_INET:
+		if (m->m_pkthdr.csum_flags & (M_CSUM_TCPv4|M_CSUM_UDPv4)) {
+			in_undefer_cksum_tcpudp(m);
+			m->m_pkthdr.csum_flags &= ~(M_CSUM_TCPv4|M_CSUM_UDPv4);
+		}
+
+		ip = mtod(m, struct ip *);
+		ip->ip_sum = 0;
+		ip->ip_sum = in_cksum(m, ip->ip_hl << 2);
+		break;
+#ifdef INET6
+	case AF_INET6:
+		if (m->m_pkthdr.csum_flags & (M_CSUM_TCPv6|M_CSUM_UDPv6)) {
+			in6_undefer_cksum_tcpudp(m);
+			m->m_pkthdr.csum_flags &= ~(M_CSUM_TCPv6|M_CSUM_UDPv6);
+		}
+		break;
+#endif
+	}
+}
+
+static int
+npf_route_output(struct ifnet *ifp, struct mbuf *m, struct sockaddr *to)
+{
+	unsigned len;
+
+	if (! (ifp->if_flags & IFF_UP)) {
+		m_freem(m);
+		return ENETDOWN;
+	}
+
+#ifdef INET6
+	if (to->sa_family == AF_INET6) {
+		in6_setscope(&((struct sockaddr_in6 *)to)->sin6_addr,
+		    ifp, NULL);
+	}
+#endif
+
+	KASSERT((m->m_flags & M_PKTHDR) != 0);
+	len = (unsigned)m->m_pkthdr.len;
+	if (len > ifp->if_mtu) {
+		m_freem(m);
+		return EMSGSIZE;
+	}
+
+	return if_output_lock(ifp, ifp, m, to, NULL);
+}
+
+static bool
+npf_route(npf_cache_t *npc, void *meta, const npf_match_info_t *mi, int *decision)
+{
+	struct mbuf *m, *m0 = nbuf_head_mbuf(npc->npc_nbuf);
+	const npf_ext_route_t *route = meta;
+	struct sockaddr_storage ss;
+	const struct sockaddr *dst = NULL;
+	ifnet_t *ifp;
+	struct psref psref;
+	int bound;
+	int family, socklen;
+
+	KASSERT(the_route == NULL || route == the_route);
+
+	/* 
+	 * should be the gateway on the target interface
+	 * but we cannot have a route here
+	 */
+	switch (npc->npc_alen) {
+	case sizeof(struct in_addr):
+		family = AF_INET;
+		socklen = sizeof(struct sockaddr_in);
+		if (route->gateway != NULL)
+			dst = sstocsa(&route->nexthop);
+		break;
+	case sizeof(struct in6_addr):
+		family = AF_INET6;
+		socklen = sizeof(struct sockaddr_in6);
+		if (route->gateway6 != NULL)
+			dst = sstocsa(&route->nexthop6);
+		break;
+	default:
+		return false;
+	}
+
+	if (dst != NULL) {
+	 	/* copy address from nexthop */
+		memcpy(&ss, dst, socklen);
+	} else {
+		/* take destination from packet */
+		npf_route_nexthop(npc, &ss, family, socklen);
+	}
+
+	m = m_dup(m0, 0, M_COPYALL, M_NOWAIT);
+	if (m == NULL)
+		return false;
+
+	npf_route_checksums(family, m);
+
+	bound = curlwp_bind();
+	ifp = if_get(route->if_name, &psref);
+	if (ifp == NULL) {
+		curlwp_bindx(bound);
+		m_freem(m);
+		return false;
+	}
+
+	(void)npf_route_output(ifp, m, sstosa(&ss));
+
+	if_put(ifp, &psref);
+	curlwp_bindx(bound);
+
+	if (route->duplicate)
+		return true;
+
+	*decision = NPF_DECISION_PASS;
+	return false;
+}
+
+__dso_public int
+npf_ext_route_init(npf_t *npf)
+{
+        static const npf_ext_ops_t npf_route_ops = {
+                .version        = NPFEXT_ROUTE_VER,
+                .ctx            = NULL,
+                .ctor           = npf_route_ctor,
+                .dtor           = npf_route_dtor,
+                .proc           = npf_route
+        };
+        npf_ext_route_id = npf_ext_register(npf, "route",
+	    &npf_route_ops);
+        return npf_ext_route_id ? 0 : EEXIST;
+}
+
+__dso_public int
+npf_ext_route_fini(npf_t *npf)
+{
+
+        return npf_ext_unregister(npf, npf_ext_route_id);
+}
+
+#ifdef _KERNEL
+static int
+npf_ext_route_modcmd(modcmd_t cmd, void *arg)
+{
+        npf_t *npf = npf_getkernctx();
+ 
+        switch (cmd) {
+        case MODULE_CMD_INIT:
+                return npf_ext_route_init(npf);
+        case MODULE_CMD_FINI:
+                return npf_ext_route_fini(npf);  
+        case MODULE_CMD_AUTOUNLOAD:
+                return npf_autounload_p() ? 0 : EBUSY;
+        default:
+                return ENOTTY;
+        }
+        return 0;
+}
+#endif  
Index: npfkern.h
===================================================================
RCS file: /cvsroot/src/sys/net/npf/npfkern.h,v
retrieving revision 1.6
diff -p -u -r1.6 npfkern.h
--- npfkern.h	1 Jul 2025 18:42:37 -0000	1.6
+++ npfkern.h	27 Mar 2026 14:13:42 -0000
@@ -101,6 +101,9 @@ int	npf_ext_normalize_fini(npf_t *);
 int	npf_ext_rndblock_init(npf_t *);
 int	npf_ext_rndblock_fini(npf_t *);
 
+int	npf_ext_route_init(npf_t *);
+int	npf_ext_route_fini(npf_t *);
+
 /*
  * ALGs.
  */
