From: Joas Antonio dos Santos <joasantonio108@gmail.com>
To: Pablo Neira Ayuso <pablo@netfilter.org>, Florian Westphal <fw@strlen.de>
Cc: Phil Sutter <phil@nwl.cc>,
netfilter-devel@vger.kernel.org, coreteam@netfilter.org,
netdev@vger.kernel.org
Subject: [PATCH nf-next v3] netfilter: ip6t_NPT: ensure skb is writable before header modification
Date: Wed, 07 Oct 2026 12:48:51 -0300 [thread overview]
Message-ID: <179138813128.81565.3404247396030440460@gmail.com> (raw)
ip6t_snpt_tg() and ip6t_dnpt_tg() rewrite the IPv6 source/destination
address in place via ip6t_npt_map_pfx() without first calling
skb_ensure_writable(). When the skb data is shared with a clone, the
write is visible to the other holder of the buffer.
This is easy to hit with DNPT in PREROUTING: a packet socket on the
ingress interface (tcpdump, or any AF_PACKET listener) queues a clone
of the incoming skb, and DNPT then rewrites the shared data, so the
captured copy shows the translated destination instead of the one that
was on the wire.
The ICMPv6 error path has the same problem for the embedded IPv6
header, and a second one: icmpv6_bounced_ipv6hdr() uses
skb_header_pointer(), which returns a pointer to a stack copy when the
header is not in the linear area. ip6t_npt_map_pfx() then modifies
the copy, so the inner header of such ICMPv6 errors is never
translated.
Call skb_ensure_writable() on the IPv6 header before translating it,
and make icmpv6_bounced_ipv6hdr() ensure the embedded header is
writable and return a pointer into the skb instead of a stack copy.
Fixes: 8a91bb0c304b ("netfilter: ip6tables: add stateless IPv6-to-IPv6 Network Prefix Translation target")
Fixes: d5608a0578a0 ("netfilter: ip6t_NPT: rewrite addresses in ICMPv6 original packet")
Signed-off-by: Joas Antonio dos Santos <joasantonio108@gmail.com>
Assisted-by: Claude:claude-opus-5-5
---
Changes in v3:
- v2 was hand-edited and does not apply (corrupt hunk); regenerated
with git format-patch on top of current mainline.
- Keep icmpv6_bounced_ipv6hdr() and make it return a writable pointer
into the skb instead of open-coding it twice.
- Commit message: describe the reproducer below instead of the
earlier test notes.
Reproducer (tested on 602042bf29f6, arm64, QEMU):
ip netns add C; ip netns add R; ip netns add S
ip link add cr type veth peer name rc
ip link add rs type veth peer name sr
ip link set cr netns C; ip link set rc netns R
ip link set rs netns R; ip link set sr netns S
ip -n C -6 addr add fd00:1::2/64 dev cr nodad; ip -n C link set cr up
ip -n R -6 addr add fd00:1::1/64 dev rc nodad; ip -n R link set rc up
ip -n R -6 addr add fd00:2::1/64 dev rs nodad; ip -n R link set rs up
ip -n S -6 addr add fd00:2::2/64 dev sr nodad; ip -n S link set sr up
ip -n C -6 route add default via fd00:1::1
ip -n S -6 route add fd00:99::/64 via fd00:2::1
ip netns exec R sysctl -w net.ipv6.conf.all.forwarding=1
ip netns exec R ip6tables -t mangle -A PREROUTING -i rs \
-d fd00:99::/64 -j DNPT --src-pfx fd00:99::/64 --dst-pfx fd00:1::/64
# AF_PACKET listener on rs that reads its queue 4s later (cap.c below)
ip netns exec R ./cap rs 4 &
sleep 1
# any UDP sender works; send6.c below sends 3 datagrams to port 9999
ip netns exec S ./send6 fd00:99::2
wait
before: captured UDP packet, dst=fd00:1::98:0:0:2 (rewritten in the clone)
after: captured UDP packet, dst=fd00:99::2 (as sent on the wire)
cap.c:
#include <arpa/inet.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <netinet/ip6.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv)
{
struct sockaddr_ll ll = { .sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_IPV6),
.sll_ifindex = if_nametoindex(argv[1]) };
int s = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_IPV6));
char buf[2048], dst[64];
bind(s, (void *)&ll, sizeof(ll));
sleep(atoi(argv[2]));
for (;;) {
struct ip6_hdr *h = (void *)buf;
int n = recv(s, buf, sizeof(buf), MSG_DONTWAIT);
if (n < (int)sizeof(*h))
break;
if (h->ip6_nxt != IPPROTO_UDP)
continue;
inet_ntop(AF_INET6, &h->ip6_dst, dst, sizeof(dst));
printf("captured UDP packet, dst=%s\n", dst);
}
return 0;
}
send6.c:
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv)
{
struct sockaddr_in6 a = { .sin6_family = AF_INET6,
.sin6_port = htons(9999) };
int s = socket(AF_INET6, SOCK_DGRAM, 0);
inet_pton(AF_INET6, argv[1], &a.sin6_addr);
for (int i = 0; i < 3; i++) {
sendto(s, "npt", 3, 0, (void *)&a, sizeof(a));
usleep(200000);
}
return 0;
}
Kernel config: IP6_NF_IPTABLES, IP6_NF_MANGLE, IP6_NF_TARGET_NPT, PACKET.
Builds cleanly with W=1.
net/ipv6/netfilter/ip6t_NPT.c | 27 +++++++++++++++++----------
1 file changed, 17 insertions(+), 10 deletions(-)
diff --git a/net/ipv6/netfilter/ip6t_NPT.c b/net/ipv6/netfilter/ip6t_NPT.c
index 787c74aa8..e76d202dd 100644
--- a/net/ipv6/netfilter/ip6t_NPT.c
+++ b/net/ipv6/netfilter/ip6t_NPT.c
@@ -77,29 +77,34 @@ static bool ip6t_npt_map_pfx(const struct ip6t_npt_tginfo *npt,
return true;
}
-static struct ipv6hdr *icmpv6_bounced_ipv6hdr(struct sk_buff *skb,
- struct ipv6hdr *_bounced_hdr)
+static struct ipv6hdr *icmpv6_bounced_ipv6hdr(struct sk_buff *skb)
{
+ unsigned int offset;
+
if (ipv6_hdr(skb)->nexthdr != IPPROTO_ICMPV6)
return NULL;
if (!icmpv6_is_err(icmp6_hdr(skb)->icmp6_type))
return NULL;
- return skb_header_pointer(skb,
- skb_transport_offset(skb) + sizeof(struct icmp6hdr),
- sizeof(struct ipv6hdr),
- _bounced_hdr);
+ offset = skb_transport_offset(skb) + sizeof(struct icmp6hdr);
+ if (skb_ensure_writable(skb, offset + sizeof(struct ipv6hdr)))
+ return NULL;
+
+ return (struct ipv6hdr *)(skb_transport_header(skb) +
+ sizeof(struct icmp6hdr));
}
static unsigned int
ip6t_snpt_tg(struct sk_buff *skb, const struct xt_action_param *par)
{
const struct ip6t_npt_tginfo *npt = par->targinfo;
- struct ipv6hdr _bounced_hdr;
struct ipv6hdr *bounced_hdr;
struct in6_addr bounced_pfx;
+ if (skb_ensure_writable(skb, sizeof(struct ipv6hdr)))
+ return NF_DROP;
+
if (!ip6t_npt_map_pfx(npt, &ipv6_hdr(skb)->saddr)) {
icmpv6_send(skb, ICMPV6_PARAMPROB, ICMPV6_HDR_FIELD,
offsetof(struct ipv6hdr, saddr));
@@ -107,7 +112,7 @@ ip6t_snpt_tg(struct sk_buff *skb, const struct xt_action_param *par)
}
/* rewrite dst addr of bounced packet which was sent to dst range */
- bounced_hdr = icmpv6_bounced_ipv6hdr(skb, &_bounced_hdr);
+ bounced_hdr = icmpv6_bounced_ipv6hdr(skb);
if (bounced_hdr) {
ipv6_addr_prefix(&bounced_pfx, &bounced_hdr->daddr, npt->src_pfx_len);
if (ipv6_addr_cmp(&bounced_pfx, &npt->src_pfx.in6) == 0)
@@ -121,10 +126,12 @@ static unsigned int
ip6t_dnpt_tg(struct sk_buff *skb, const struct xt_action_param *par)
{
const struct ip6t_npt_tginfo *npt = par->targinfo;
- struct ipv6hdr _bounced_hdr;
struct ipv6hdr *bounced_hdr;
struct in6_addr bounced_pfx;
+ if (skb_ensure_writable(skb, sizeof(struct ipv6hdr)))
+ return NF_DROP;
+
if (!ip6t_npt_map_pfx(npt, &ipv6_hdr(skb)->daddr)) {
icmpv6_send(skb, ICMPV6_PARAMPROB, ICMPV6_HDR_FIELD,
offsetof(struct ipv6hdr, daddr));
@@ -132,7 +139,7 @@ ip6t_dnpt_tg(struct sk_buff *skb, const struct xt_action_param *par)
}
/* rewrite src addr of bounced packet which was sent from dst range */
- bounced_hdr = icmpv6_bounced_ipv6hdr(skb, &_bounced_hdr);
+ bounced_hdr = icmpv6_bounced_ipv6hdr(skb);
if (bounced_hdr) {
ipv6_addr_prefix(&bounced_pfx, &bounced_hdr->saddr, npt->src_pfx_len);
if (ipv6_addr_cmp(&bounced_pfx, &npt->src_pfx.in6) == 0)
--
2.43.0
reply other threads:[~2026-10-07 15:49 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
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=179138813128.81565.3404247396030440460@gmail.com \
--to=joasantonio108@gmail.com \
--cc=coreteam@netfilter.org \
--cc=fw@strlen.de \
--cc=netdev@vger.kernel.org \
--cc=netfilter-devel@vger.kernel.org \
--cc=pablo@netfilter.org \
--cc=phil@nwl.cc \
/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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox