From: Florian Westphal <fw@strlen.de>
To: <netfilter-devel@vger.kernel.org>
Cc: Florian Westphal <fw@strlen.de>
Subject: [PATCH v3 nf-next 1/3] netfilter: nfnetlink_queue: restrict writes to network header
Date: Tue, 9 Jun 2026 13:51:53 +0200 [thread overview]
Message-ID: <20260609115201.2563-2-fw@strlen.de> (raw)
In-Reply-To: <20260609115201.2563-1-fw@strlen.de>
nfnetlink_queue doesn't allow selective replacements of some part of the
payload, only complete replacement.
If the new data is shorter, skb is trimmed, otherwise expanded.
Add minimal validation of the new ip/ipv6 header. Check total len
matches skb length. Disallow ip option modifications after prerouting.
IPv6 extension headers are also disabled.
IP options and exthdrs could be allowed later after validation pass or
ip option recompile.
Transport header is not checked.
Bridge modifications are rejected. Given userspace doesn't even receive
L2 headers, use is limited and I don't think there are any users of
bridge nfnetlink_queue, let alone users that modifiy payload.
Arp isn't supported at all.
Signed-off-by: Florian Westphal <fw@strlen.de>
---
v3: don't support bridge writes for now; I don't think there are users.
net/netfilter/nfnetlink_queue.c | 170 ++++++++++++++++++++++++++++++++
1 file changed, 170 insertions(+)
diff --git a/net/netfilter/nfnetlink_queue.c b/net/netfilter/nfnetlink_queue.c
index 60ab88d45096..48fdef7ef145 100644
--- a/net/netfilter/nfnetlink_queue.c
+++ b/net/netfilter/nfnetlink_queue.c
@@ -1136,6 +1136,173 @@ nfqnl_enqueue_packet(struct nf_queue_entry *entry, unsigned int queuenum)
return err;
}
+static bool nfqnl_validate_ipopts(const struct iphdr *iph_new,
+ const struct nf_queue_entry *e)
+{
+ const struct iphdr *iph_orig = ip_hdr(e->skb);
+ unsigned int ihl = iph_new->ihl * 4;
+
+ if (iph_new->ihl != iph_orig->ihl)
+ return false;
+ if (ihl == sizeof(*iph_orig))
+ return true;
+
+ return memcmp(iph_new + 1, ip_hdr(e->skb) + 1, ihl - sizeof(*iph_orig)) == 0;
+}
+
+static bool nfqnl_validate_ip4(const struct iphdr *iph, unsigned int data_len,
+ const struct nf_queue_entry *e)
+{
+ unsigned int ihl;
+
+ if (data_len < sizeof(*iph))
+ return false;
+
+ ihl = iph->ihl * 4u;
+ if (ihl < sizeof(*iph) || data_len < ihl)
+ return false;
+
+ if (iph->version != 4 ||
+ ((iph->frag_off ^ ip_hdr(e->skb)->frag_off) & ~htons(IP_DF)) != 0)
+ return false;
+
+ /* BIG TCP won't work; netlink attr len is u16 */
+ if (ntohs(iph->tot_len) != data_len)
+ return false;
+
+ /* support for ipopts mangling would require
+ * recompile + skb transport header update.
+ */
+ return nfqnl_validate_ipopts(iph, e);
+}
+
+static bool nfqnl_validate_one_exthdr(const u8 *data,
+ unsigned int data_len,
+ const struct nf_queue_entry *e,
+ int start, int hdrlen)
+{
+ u16 octets;
+
+ if (data_len < hdrlen || hdrlen < 2)
+ return false;
+
+ while (hdrlen > 0) {
+ if (data_len < sizeof(octets))
+ return false;
+ data_len -= sizeof(octets);
+
+ if (skb_copy_bits(e->skb, start, &octets, sizeof(octets)))
+ return false;
+
+ if (hdrlen < sizeof(octets))
+ return false;
+
+ hdrlen -= sizeof(octets);
+ if (memcmp(data, &octets, sizeof(octets)))
+ return false;
+
+ start += sizeof(octets);
+ data += sizeof(octets);
+ }
+
+ return true;
+}
+
+static bool nfqnl_validate_exthdr(const struct ipv6hdr *ip6_new,
+ unsigned int data_len,
+ const struct nf_queue_entry *e)
+{
+ const struct ipv6hdr *ip6_orig = ipv6_hdr(e->skb);
+ int exthdr_cnt = 0, start = sizeof(*ip6_orig);
+ const u8 *data = (const u8 *)ip6_new;
+ u8 orig_nexthdr = ip6_orig->nexthdr;
+ u8 new_nexthdr = ip6_new->nexthdr;
+
+ if (new_nexthdr != orig_nexthdr)
+ return false;
+
+ data += sizeof(*ip6_new);
+ data_len -= sizeof(*ip6_new);
+
+ while (ipv6_ext_hdr(orig_nexthdr)) {
+ const struct ipv6_opt_hdr *hp;
+ struct ipv6_opt_hdr _hdr;
+ int hdrlen;
+
+ if (orig_nexthdr == NEXTHDR_NONE)
+ return true;
+
+ if (unlikely(exthdr_cnt++ >= IP6_MAX_EXT_HDRS_CNT))
+ return false;
+
+ hp = skb_header_pointer(e->skb, start, sizeof(_hdr), &_hdr);
+ if (!hp)
+ return false;
+
+ switch (orig_nexthdr) {
+ case NEXTHDR_FRAGMENT:
+ hdrlen = sizeof(struct frag_hdr);
+ break;
+ case NEXTHDR_AUTH:
+ hdrlen = ipv6_authlen(hp);
+ break;
+ default:
+ hdrlen = ipv6_optlen(hp);
+ break;
+ }
+
+ if (!nfqnl_validate_one_exthdr(data, data_len, e,
+ start, hdrlen))
+ return false;
+
+ orig_nexthdr = hp->nexthdr;
+ hp = (const void *)data;
+ new_nexthdr = hp->nexthdr;
+
+ if (new_nexthdr != orig_nexthdr)
+ return false;
+
+ data_len -= hdrlen;
+ start += hdrlen;
+ data += hdrlen;
+ }
+
+ return true;
+}
+
+static bool nfqnl_validate_ip6(const struct ipv6hdr *ip6, unsigned int data_len,
+ const struct nf_queue_entry *e)
+{
+ if (data_len < sizeof(*ip6))
+ return false;
+
+ /* BIG TCP/jumbograms won't work; netlink attr len is u16 */
+ if (ntohs(ip6->payload_len) != data_len - sizeof(*ip6))
+ return false;
+
+ if (ip6->version != 6)
+ return false;
+
+ return nfqnl_validate_exthdr(ip6, data_len, e);
+}
+
+static bool nfqnl_validate_write(const void *data, unsigned int data_len,
+ const struct nf_queue_entry *e)
+{
+ switch (e->state.pf) {
+ case NFPROTO_IPV4:
+ return nfqnl_validate_ip4(data, data_len, e);
+ case NFPROTO_IPV6:
+ return nfqnl_validate_ip6(data, data_len, e) &&
+ !(IP6CB(e->skb)->flags & IP6SKB_JUMBOGRAM);
+ case NFPROTO_BRIDGE:
+ /* No write support. Bridge is dubious: userspace doesn't even see L2 header */
+ return false;
+ }
+
+ return false;
+}
+
static int
nfqnl_mangle(void *data, unsigned int data_len, struct nf_queue_entry *e, int diff)
{
@@ -1144,6 +1311,9 @@ nfqnl_mangle(void *data, unsigned int data_len, struct nf_queue_entry *e, int di
if (e->state.net->user_ns != &init_user_ns)
return -EPERM;
+ if (!nfqnl_validate_write(data, data_len, e))
+ return -EINVAL;
+
if (diff < 0) {
unsigned int min_len = skb_transport_offset(e->skb);
--
2.53.0
next prev parent reply other threads:[~2026-06-09 11:52 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-09 11:51 [PATCH v3 nf-next 0/3] netfilter: add restrictions/validations for packet rewrite Florian Westphal
2026-06-09 11:51 ` Florian Westphal [this message]
2026-06-09 11:51 ` [PATCH v3 nf-next 2/3] netfilter: nftables: restrict linklayer and network header writes Florian Westphal
2026-06-09 11:51 ` [PATCH v3 nf-next 3/3] netfilter: nftables: restrict checkum update offset Florian Westphal
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260609115201.2563-2-fw@strlen.de \
--to=fw@strlen.de \
--cc=netfilter-devel@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.