* [RFC net-next v2 01/18] lwtunnel: track callback dispatch separately from return value
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 02/18] ipv4/ipv6: add LWT receive helpers Ralf Lici
` (16 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Daniel Gröber,
Antonio Quartulli, linux-kernel
LWT used -EOPNOTSUPP both as its missing-callback sentinel and as a
valid callback error. Track whether an operation was invoked separately
so callbacks can return meaningful errors without triggering the
no-operation skb cleanup path.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
New patch in RFC v2.
net/core/lwtunnel.c | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/net/core/lwtunnel.c b/net/core/lwtunnel.c
index 8223c44f10c8..db21b9ff6f86 100644
--- a/net/core/lwtunnel.c
+++ b/net/core/lwtunnel.c
@@ -322,6 +322,7 @@ int lwtunnel_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
const struct lwtunnel_encap_ops *ops;
struct lwtunnel_state *lwtstate;
+ bool called_cb = false;
struct dst_entry *dst;
int ret;
@@ -353,13 +354,14 @@ int lwtunnel_output(struct net *net, struct sock *sk, struct sk_buff *skb)
if (likely(ops && ops->output)) {
/* Encap pushes outer headers over the metadata; drop it. */
skb_metadata_clear(skb);
+ called_cb = true;
dev_xmit_recursion_inc();
ret = ops->output(net, sk, skb);
dev_xmit_recursion_dec();
}
rcu_read_unlock();
- if (ret == -EOPNOTSUPP)
+ if (unlikely(!called_cb))
goto drop;
goto out;
@@ -377,6 +379,7 @@ int lwtunnel_xmit(struct sk_buff *skb)
{
const struct lwtunnel_encap_ops *ops;
struct lwtunnel_state *lwtstate;
+ bool called_cb = false;
struct dst_entry *dst;
int ret;
@@ -409,13 +412,14 @@ int lwtunnel_xmit(struct sk_buff *skb)
if (likely(ops && ops->xmit)) {
/* Encap pushes outer headers over the metadata; drop it. */
skb_metadata_clear(skb);
+ called_cb = true;
dev_xmit_recursion_inc();
ret = ops->xmit(skb);
dev_xmit_recursion_dec();
}
rcu_read_unlock();
- if (ret == -EOPNOTSUPP)
+ if (unlikely(!called_cb))
goto drop;
goto out;
@@ -433,6 +437,7 @@ int lwtunnel_input(struct sk_buff *skb)
{
const struct lwtunnel_encap_ops *ops;
struct lwtunnel_state *lwtstate;
+ bool called_cb = false;
struct dst_entry *dst;
int ret;
@@ -462,13 +467,14 @@ int lwtunnel_input(struct sk_buff *skb)
if (likely(ops && ops->input)) {
/* Encap pushes outer headers over the metadata; drop it. */
skb_metadata_clear(skb);
+ called_cb = true;
dev_xmit_recursion_inc();
ret = ops->input(skb);
dev_xmit_recursion_dec();
}
rcu_read_unlock();
- if (ret == -EOPNOTSUPP)
+ if (unlikely(!called_cb))
goto drop;
return ret;
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 02/18] ipv4/ipv6: add LWT receive helpers
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 01/18] lwtunnel: track callback dispatch separately from return value Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 03/18] ipv6: allow output fragmentation to reuse a Fragment Header Ralf Lici
` (15 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Daniel Gröber,
Antonio Quartulli, Toke Høiland-Jørgensen,
linux-kernel
Cross-family LWT translation constructs a validated target-family
packet and needs to enter its receive path at PRE_ROUTING. Add helpers
that invoke the hook and continue with normal input routing.
The continuation remains in IP core, so it stays valid if NFQUEUE holds
a packet after the selecting LWT dst has been dropped.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
New patch in RFC v2.
include/net/ip.h | 1 +
include/net/ipv6.h | 1 +
net/ipv4/ip_input.c | 10 ++++++++++
net/ipv6/ip6_input.c | 11 +++++++++++
4 files changed, 23 insertions(+)
diff --git a/include/net/ip.h b/include/net/ip.h
index 618b9806c10b..761b6b0d48f7 100644
--- a/include/net/ip.h
+++ b/include/net/ip.h
@@ -170,6 +170,7 @@ int ip_build_and_send_pkt(struct sk_buff *skb, const struct sock *sk,
struct ip_options_rcu *opt, u8 tos);
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
struct net_device *orig_dev);
+int ip_rcv_lwt(struct sk_buff *skb);
void ip_list_rcv(struct list_head *head, struct packet_type *pt,
struct net_device *orig_dev);
int ip_local_deliver(struct sk_buff *skb);
diff --git a/include/net/ipv6.h b/include/net/ipv6.h
index 3de07e738538..e3f245d640ac 100644
--- a/include/net/ipv6.h
+++ b/include/net/ipv6.h
@@ -1016,6 +1016,7 @@ static inline __be32 flowi6_get_flowlabel(const struct flowi6 *fl6)
int ipv6_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev);
+int ip6_rcv_lwt(struct sk_buff *skb);
void ipv6_list_rcv(struct list_head *head, struct packet_type *pt,
struct net_device *orig_dev);
diff --git a/net/ipv4/ip_input.c b/net/ipv4/ip_input.c
index 9860178752b8..f679e38a1a1c 100644
--- a/net/ipv4/ip_input.c
+++ b/net/ipv4/ip_input.c
@@ -493,6 +493,16 @@ static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
return ret;
}
+/* enter IPv4 PRE_ROUTING with a validated LWT packet */
+int ip_rcv_lwt(struct sk_buff *skb)
+{
+ struct net *net = dev_net(skb->dev);
+
+ return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, net, NULL, skb,
+ skb->dev, NULL, ip_rcv_finish);
+}
+EXPORT_SYMBOL_GPL(ip_rcv_lwt);
+
/*
* Main IP Receive routine.
*/
diff --git a/net/ipv6/ip6_input.c b/net/ipv6/ip6_input.c
index d332ec60f915..47aaa1fcf82b 100644
--- a/net/ipv6/ip6_input.c
+++ b/net/ipv6/ip6_input.c
@@ -26,6 +26,7 @@
#include <linux/mroute6.h>
#include <linux/slab.h>
#include <linux/indirect_call_wrapper.h>
+#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv6.h>
@@ -119,6 +120,16 @@ int ip6_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
return dst_input(skb);
}
+/* enter IPv6 PRE_ROUTING with a validated LWT packet */
+int ip6_rcv_lwt(struct sk_buff *skb)
+{
+ struct net *net = dev_net(skb->dev);
+
+ return NF_HOOK(NFPROTO_IPV6, NF_INET_PRE_ROUTING, net, NULL, skb,
+ skb->dev, NULL, ip6_rcv_finish);
+}
+EXPORT_SYMBOL_GPL(ip6_rcv_lwt);
+
static void ip6_sublist_rcv_finish(struct list_head *head)
{
struct sk_buff *skb, *next;
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 03/18] ipv6: allow output fragmentation to reuse a Fragment Header
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 01/18] lwtunnel: track callback dispatch separately from return value Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 02/18] ipv4/ipv6: add LWT receive helpers Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 04/18] netfilter: preserve IPv6 refragmentation state across reassembly Ralf Lici
` (14 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Daniel Gröber,
Antonio Quartulli, Kuniyuki Iwashima, Alice Mikityanska,
linux-kernel
IPv4-to-IPv6 translation constructs an IPv6 Fragment Header before the
target route is selected. If that route has a smaller MTU, IPv6 output
must split the translated fragment again. Adding another Fragment Header
would create a new fragment domain instead of subfragmenting the
original datagram representation.
Allow ip6_fragment to reuse a locally constructed Fragment Header,
preserving its identification, datagram-relative offset and More
Fragments state. This mirrors IPv4 refragmentation, whose fragment state
carries the original offset and whether more fragments follow the input
fragment.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
New patch in RFC v2 replacing ipxlat's v1 IPv4 pre-fragmentation
machinery.
include/linux/ipv6.h | 2 ++
include/net/ipv6.h | 1 +
net/ipv6/ip6_output.c | 53 +++++++++++++++++++++++++++++++++++++++----
3 files changed, 51 insertions(+), 5 deletions(-)
diff --git a/include/linux/ipv6.h b/include/linux/ipv6.h
index a7421382a916..558260074842 100644
--- a/include/linux/ipv6.h
+++ b/include/linux/ipv6.h
@@ -177,6 +177,8 @@ struct inet6_skb_parm {
#define IP6SKB_L3SLAVE 64
#define IP6SKB_JUMBOGRAM 128
#define IP6SKB_SEG6 256
+/* the local stack owns fragmentation of this IPv6 representation */
+#define IP6SKB_FRAG_SOURCE 512
#define IP6SKB_MULTIPATH 1024
#define IP6SKB_MCROUTE 2048
};
diff --git a/include/net/ipv6.h b/include/net/ipv6.h
index e3f245d640ac..a1cca04fd17a 100644
--- a/include/net/ipv6.h
+++ b/include/net/ipv6.h
@@ -189,6 +189,7 @@ struct ip6_frag_state {
int hroom;
int troom;
__be32 frag_id;
+ bool not_last_frag;
u8 nexthdr;
};
diff --git a/net/ipv6/ip6_output.c b/net/ipv6/ip6_output.c
index 25fe0ba98b1a..73633483f1ab 100644
--- a/net/ipv6/ip6_output.c
+++ b/net/ipv6/ip6_output.c
@@ -204,7 +204,8 @@ static int __ip6_finish_output(struct net *net, struct sock *sk, struct sk_buff
return ip6_finish_output_gso(net, sk, skb, mtu);
if (unlikely(skb->len > mtu ||
- (IP6CB(skb)->frag_max_size && skb->len > IP6CB(skb)->frag_max_size)))
+ (IP6CB(skb)->frag_max_size &&
+ skb->len > IP6CB(skb)->frag_max_size)))
return ip6_fragment(net, sk, skb, ip6_finish_output2);
return ip6_finish_output2(net, sk, skb);
@@ -805,6 +806,7 @@ void ip6_frag_init(struct sk_buff *skb, unsigned int hlen, unsigned int mtu,
state->troom = needed_tailroom;
state->offset = 0;
+ state->not_last_frag = false;
}
EXPORT_SYMBOL(ip6_frag_init);
@@ -873,7 +875,7 @@ struct sk_buff *ip6_frag_next(struct sk_buff *skb, struct ip6_frag_state *state)
state->left -= len;
fh->frag_off = htons(state->offset);
- if (state->left > 0)
+ if (state->left > 0 || state->not_last_frag)
fh->frag_off |= htons(IP6_MF);
ipv6_hdr(frag)->payload_len = htons(frag->len - sizeof(struct ipv6hdr));
@@ -891,10 +893,13 @@ int ip6_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
struct rt6_info *rt = dst_rt6_info(skb_dst(skb));
struct ipv6_pinfo *np = skb->sk && !dev_recursion_level() ?
inet6_sk(skb->sk) : NULL;
+ struct frag_hdr old_fh;
u8 tstamp_type = skb->tstamp_type;
struct ip6_frag_state state;
unsigned int mtu, hlen, nexthdr_offset;
+ unsigned int old_data_len, old_offset, unfrag_len;
ktime_t tstamp = skb->tstamp;
+ bool frag_source, refrag;
int hroom, err = 0;
__be32 frag_id;
u8 *prevhdr, nexthdr = 0;
@@ -910,6 +915,31 @@ int ip6_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
hlen = err;
nexthdr = *prevhdr;
nexthdr_offset = prevhdr - skb_network_header(skb);
+ frag_source = IP6CB(skb)->flags & IP6SKB_FRAG_SOURCE;
+ refrag = frag_source && nexthdr == NEXTHDR_FRAGMENT;
+ if (unlikely(refrag)) {
+ /* The current fragment header describes this locally built
+ * IPv6 representation. We need to replace it with smaller
+ * fragments rather than nesting another frag hdr around it.
+ */
+ if (unlikely(skb->len < hlen + sizeof(old_fh) ||
+ skb_copy_bits(skb, skb_network_offset(skb) + hlen,
+ &old_fh, sizeof(old_fh)))) {
+ err = -EINVAL;
+ goto fail;
+ }
+
+ old_data_len = skb->len - hlen - sizeof(old_fh);
+ old_offset = ntohs(old_fh.frag_off) & IP6_OFFSET;
+ unfrag_len = hlen - sizeof(struct ipv6hdr);
+ if (unlikely(old_offset + unfrag_len > IPV6_MAXPLEN ||
+ old_data_len >
+ IPV6_MAXPLEN - old_offset - unfrag_len)) {
+ err = -EINVAL;
+ goto fail;
+ }
+ nexthdr = old_fh.nexthdr;
+ }
mtu = ip6_skb_dst_mtu(skb);
@@ -939,8 +969,11 @@ int ip6_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
goto fail_toobig;
mtu -= hlen + sizeof(struct frag_hdr);
- frag_id = ipv6_select_ident(net, &ipv6_hdr(skb)->daddr,
- &ipv6_hdr(skb)->saddr);
+ if (refrag)
+ frag_id = old_fh.identification;
+ else
+ frag_id = ipv6_select_ident(net, &ipv6_hdr(skb)->daddr,
+ &ipv6_hdr(skb)->saddr);
if (skb->ip_summed == CHECKSUM_PARTIAL &&
(err = skb_checksum_help(skb)))
@@ -953,7 +986,8 @@ int ip6_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
struct ip6_fraglist_iter iter;
struct sk_buff *frag2;
- if (first_len - hlen > mtu ||
+ if (refrag ||
+ first_len - hlen > mtu ||
((first_len - hlen) & 7) ||
skb_cloned(skb) ||
skb_headroom(skb) < (hroom + sizeof(struct frag_hdr)))
@@ -1038,6 +1072,15 @@ int ip6_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
ip6_frag_init(skb, hlen, mtu, rt->dst.dev->needed_tailroom,
LL_RESERVED_SPACE(rt->dst.dev), prevhdr, nexthdr, frag_id,
&state);
+ if (refrag) {
+ /* Mirror IPv4 refragmentation: start at the old datagram
+ * offset and retain its MF value on the final new fragment.
+ */
+ state.left -= sizeof(old_fh);
+ state.ptr += sizeof(old_fh);
+ state.offset = old_offset;
+ state.not_last_frag = old_fh.frag_off & htons(IP6_MF);
+ }
/*
* Keep copying data until we run out.
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 04/18] netfilter: preserve IPv6 refragmentation state across reassembly
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (2 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 03/18] ipv6: allow output fragmentation to reuse a Fragment Header Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 05/18] net: ipxlat: add engine skeleton and configuration model Ralf Lici
` (13 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Daniel Gröber,
Antonio Quartulli, Pablo Neira Ayuso, Florian Westphal,
Phil Sutter, netfilter-devel, coreteam, linux-kernel
Remember when all fragments describe a locally constructed IPv6
representation. Restore that state after conntrack reassembly so
IPv6 output can fragment the datagram for its actual route without
treating the old fragment size as a PMTU constraint.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
New patch in RFC v2.
include/net/ipv6_frag.h | 2 ++
net/ipv6/netfilter/nf_conntrack_reasm.c | 11 ++++++++++-
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/include/net/ipv6_frag.h b/include/net/ipv6_frag.h
index 41d9fc6965f9..df0a6309b342 100644
--- a/include/net/ipv6_frag.h
+++ b/include/net/ipv6_frag.h
@@ -26,6 +26,7 @@ struct frag_queue {
int iif;
__u16 nhoffset;
u8 ecn;
+ bool frag_source;
};
#if IS_ENABLED(CONFIG_IPV6)
@@ -36,6 +37,7 @@ static inline void ip6frag_init(struct inet_frag_queue *q, const void *a)
q->key.v6 = *key;
fq->ecn = 0;
+ fq->frag_source = true;
}
static inline u32 ip6frag_key_hashfn(const void *data, u32 len, u32 seed)
diff --git a/net/ipv6/netfilter/nf_conntrack_reasm.c b/net/ipv6/netfilter/nf_conntrack_reasm.c
index be72c4346f8b..dee89aaf56ee 100644
--- a/net/ipv6/netfilter/nf_conntrack_reasm.c
+++ b/net/ipv6/netfilter/nf_conntrack_reasm.c
@@ -272,6 +272,7 @@ static int nf_ct_frag6_queue(struct frag_queue *fq, struct sk_buff *skb,
fq->q.tstamp_type = skb->tstamp_type;
fq->q.meat += skb->len;
fq->ecn |= ecn;
+ fq->frag_source &= !!(IP6CB(skb)->flags & IP6SKB_FRAG_SOURCE);
if (payload_len > fq->q.max_size)
fq->q.max_size = payload_len;
add_frag_mem_limit(fq->q.fqdir, skb->truesize);
@@ -360,8 +361,16 @@ static int nf_ct_frag6_reasm(struct frag_queue *fq, struct sk_buff *skb,
skb->dev = dev;
ipv6_hdr(skb)->payload_len = htons(payload_len);
ipv6_change_dsfield(ipv6_hdr(skb), 0xff, ecn);
- IP6CB(skb)->frag_max_size = sizeof(struct ipv6hdr) + fq->q.max_size;
IP6CB(skb)->flags |= IP6SKB_FRAGMENTED;
+ if (fq->frag_source) {
+ /* use the output route MTU instead of the old fragment size */
+ IP6CB(skb)->frag_max_size = 0;
+ IP6CB(skb)->flags |= IP6SKB_FRAG_SOURCE;
+ } else {
+ IP6CB(skb)->frag_max_size = sizeof(struct ipv6hdr) +
+ fq->q.max_size;
+ IP6CB(skb)->flags &= ~IP6SKB_FRAG_SOURCE;
+ }
/* Yes, and fold redundant checksum back. 8) */
if (skb->ip_summed == CHECKSUM_COMPLETE)
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 05/18] net: ipxlat: add engine skeleton and configuration model
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (3 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 04/18] netfilter: preserve IPv6 refragmentation state across reassembly Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 06/18] net: ipxlat: add RFC 6052 and EAM address translation Ralf Lici
` (12 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Daniel Gröber,
Antonio Quartulli, Toke Høiland-Jørgensen,
linux-kernel
Add the reusable ipxlat module, immutable RFC 6052/EAM configuration
structures and validation helpers. Attachment-specific configuration
is intentionally left for a later patch.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-2-ralf@mandelbit.com/
- Move ipxlat from drivers/net to net/ipxlat, remove the virtual
netdevice and generic-netlink control plane, and make the engine
independent of any attachment. (Toke)
- Replace mutable per-device values with validated immutable RFC 6052
and EAM configuration structures supplied by the caller.
include/net/ipxlat.h | 66 +++++++++++++++++++++++++++++++++++
net/Kconfig | 1 +
net/Makefile | 1 +
net/ipxlat/Kconfig | 14 ++++++++
net/ipxlat/Makefile | 8 +++++
net/ipxlat/config.c | 82 ++++++++++++++++++++++++++++++++++++++++++++
net/ipxlat/core.c | 17 +++++++++
7 files changed, 189 insertions(+)
create mode 100644 include/net/ipxlat.h
create mode 100644 net/ipxlat/Kconfig
create mode 100644 net/ipxlat/Makefile
create mode 100644 net/ipxlat/config.c
create mode 100644 net/ipxlat/core.c
diff --git a/include/net/ipxlat.h b/include/net/ipxlat.h
new file mode 100644
index 000000000000..6b8efb2669b8
--- /dev/null
+++ b/include/net/ipxlat.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#ifndef _NET_IPXLAT_H_
+#define _NET_IPXLAT_H_
+
+#include <linux/in6.h>
+#include <linux/types.h>
+
+struct ipxlat_prefix4 {
+ __be32 addr;
+ u8 len;
+};
+
+struct ipxlat_prefix6 {
+ struct in6_addr addr;
+ u8 len;
+};
+
+struct ipxlat_eam {
+ struct ipxlat_prefix4 prefix4;
+ struct ipxlat_prefix6 prefix6;
+};
+
+/**
+ * struct ipxlat_config - immutable translation configuration
+ * @rfc6052_prefix6: RFC 6052 fallback prefix
+ * @has_eam: whether @eam is present
+ * @eam: optional RFC 7757 explicit address mapping
+ */
+struct ipxlat_config {
+ struct ipxlat_prefix6 rfc6052_prefix6;
+ bool has_eam;
+ struct ipxlat_eam eam;
+};
+
+/**
+ * ipxlat_prefix6_len_valid - check an RFC 6052 prefix length
+ * @len: prefix length in bits
+ *
+ * Return: true when @len is one of the RFC 6052 prefix lengths.
+ */
+bool ipxlat_prefix6_len_valid(u8 len);
+
+/**
+ * ipxlat_config_validate - validate a complete translation configuration
+ * @cfg: configuration to validate
+ *
+ * If present, the EAM's IPv6 prefix must provide enough host bits for its IPv4
+ * prefix.
+ *
+ * Return: 0 when valid, otherwise a negative errno.
+ */
+int ipxlat_config_validate(const struct ipxlat_config *cfg);
+
+#endif /* _NET_IPXLAT_H_ */
diff --git a/net/Kconfig b/net/Kconfig
index 76ab44aa439a..c4d8b9a707aa 100644
--- a/net/Kconfig
+++ b/net/Kconfig
@@ -135,6 +135,7 @@ config INET
if INET
source "net/ipv4/Kconfig"
source "net/ipv6/Kconfig"
+source "net/ipxlat/Kconfig"
source "net/netlabel/Kconfig"
source "net/mptcp/Kconfig"
diff --git a/net/Makefile b/net/Makefile
index 5b2dd7f07a85..ea92d88706e3 100644
--- a/net/Makefile
+++ b/net/Makefile
@@ -20,6 +20,7 @@ obj-$(CONFIG_XFRM) += xfrm/
obj-$(CONFIG_UNIX) += unix/
obj-$(CONFIG_INET_PSP) += psp/
obj-y += ipv6/
+obj-$(CONFIG_NET_IPXLAT) += ipxlat/
obj-$(CONFIG_PACKET) += packet/
obj-$(CONFIG_NET_KEY) += key/
obj-$(CONFIG_BRIDGE) += bridge/
diff --git a/net/ipxlat/Kconfig b/net/ipxlat/Kconfig
new file mode 100644
index 000000000000..6b0e47f8ce90
--- /dev/null
+++ b/net/ipxlat/Kconfig
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# IPXLAT - Stateless IP/ICMP Translation (SIIT)
+#
+
+config NET_IPXLAT
+ tristate "IPv6<>IPv4 packet translation engine (SIIT)"
+ depends on IPV6
+ help
+ Reusable stateless IP/ICMP packet translation engine implementing
+ RFC 7915. Useful for IPv6 focused networks. Particularly NAT64,
+ SIIT-DC, 464XLAT network architectures.
+
+ To compile this engine as a module, choose M here.
diff --git a/net/ipxlat/Makefile b/net/ipxlat/Makefile
new file mode 100644
index 000000000000..47d00b7b05bb
--- /dev/null
+++ b/net/ipxlat/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+
+obj-$(CONFIG_NET_IPXLAT) += ipxlat.o
+
+ipxlat-objs += config.o
+ipxlat-objs += core.o
diff --git a/net/ipxlat/config.c b/net/ipxlat/config.c
new file mode 100644
index 000000000000..06bd6fdc6c21
--- /dev/null
+++ b/net/ipxlat/config.c
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <linux/errno.h>
+#include <linux/inetdevice.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/module.h>
+#include <net/ipv6.h>
+
+#include <net/ipxlat.h>
+
+bool ipxlat_prefix6_len_valid(u8 len)
+{
+ switch (len) {
+ case 32:
+ case 40:
+ case 48:
+ case 56:
+ case 64:
+ case 96:
+ return true;
+ default:
+ return false;
+ }
+}
+EXPORT_SYMBOL_GPL(ipxlat_prefix6_len_valid);
+
+static bool ipxlat_v4_prefix_canonical(const struct ipxlat_prefix4 *prefix)
+{
+ __be32 mask = inet_make_mask(prefix->len);
+
+ return (prefix->addr & mask) == prefix->addr;
+}
+
+static bool ipxlat_v6_prefix_canonical(const struct ipxlat_prefix6 *prefix)
+{
+ struct in6_addr masked = prefix->addr;
+
+ ipv6_addr_prefix(&masked, &prefix->addr, prefix->len);
+ return ipv6_addr_equal(&masked, &prefix->addr);
+}
+
+int ipxlat_config_validate(const struct ipxlat_config *cfg)
+{
+ u8 host4, host6;
+
+ if (!ipxlat_prefix6_len_valid(cfg->rfc6052_prefix6.len))
+ return -EINVAL;
+ if (!ipxlat_v6_prefix_canonical(&cfg->rfc6052_prefix6))
+ return -EINVAL;
+ if (cfg->rfc6052_prefix6.addr.s6_addr[8])
+ return -EINVAL;
+
+ if (!cfg->has_eam)
+ return 0;
+
+ if (cfg->eam.prefix4.len > 32 || cfg->eam.prefix6.len > 128)
+ return -EINVAL;
+ if (!ipxlat_v4_prefix_canonical(&cfg->eam.prefix4) ||
+ !ipxlat_v6_prefix_canonical(&cfg->eam.prefix6))
+ return -EINVAL;
+
+ host4 = 32 - cfg->eam.prefix4.len;
+ host6 = 128 - cfg->eam.prefix6.len;
+ /* every IPv4 host bit must fit into the mapped IPv6 host space */
+ if (host4 > host6)
+ return -EINVAL;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(ipxlat_config_validate);
diff --git a/net/ipxlat/core.c b/net/ipxlat/core.c
new file mode 100644
index 000000000000..85037446291e
--- /dev/null
+++ b/net/ipxlat/core.c
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <linux/module.h>
+
+MODULE_DESCRIPTION("IPv6<>IPv4 stateless translation engine (SIIT)");
+MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 06/18] net: ipxlat: add RFC 6052 and EAM address translation
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (4 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 05/18] net: ipxlat: add engine skeleton and configuration model Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 07/18] net: ipxlat: add packet metadata and header helpers Ralf Lici
` (11 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Daniel Gröber, Antonio Quartulli,
Beniamino Galvani, linux-kernel
Translate each endpoint through a matching RFC 7757 EAM first and
use the configured RFC 6052 prefix as fallback. Permit the RFC 7915
dummy IPv4 source only for otherwise unmappable ICMPv6 errors.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-3-ralf@mandelbit.com/
- Extend the v1 symmetric RFC 6052 conversion with an optional RFC 7757
EAM. Each endpoint independently uses the EAM on a match and RFC 6052
otherwise, supporting both single-address and delegated-prefix CLAT.
(Beniamino)
- Validate the reserved RFC 6052 u octet during 6-to-4 extraction.
net/ipxlat/Makefile | 1 +
net/ipxlat/address.c | 218 +++++++++++++++++++++++++++++++++++++++++++
net/ipxlat/address.h | 57 +++++++++++
3 files changed, 276 insertions(+)
create mode 100644 net/ipxlat/address.c
create mode 100644 net/ipxlat/address.h
diff --git a/net/ipxlat/Makefile b/net/ipxlat/Makefile
index 47d00b7b05bb..013c338a2576 100644
--- a/net/ipxlat/Makefile
+++ b/net/ipxlat/Makefile
@@ -6,3 +6,4 @@ obj-$(CONFIG_NET_IPXLAT) += ipxlat.o
ipxlat-objs += config.o
ipxlat-objs += core.o
+ipxlat-objs += address.o
diff --git a/net/ipxlat/address.c b/net/ipxlat/address.c
new file mode 100644
index 000000000000..e1ff81daf7dc
--- /dev/null
+++ b/net/ipxlat/address.c
@@ -0,0 +1,218 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <linux/errno.h>
+#include <linux/inetdevice.h>
+#include <linux/kernel.h>
+
+#include "address.h"
+
+static bool ipxlat_prefix4_contains(const struct ipxlat_prefix4 *prefix,
+ __be32 addr)
+{
+ return (addr & inet_make_mask(prefix->len)) == prefix->addr;
+}
+
+static bool ipxlat_prefix6_contains(const struct ipxlat_prefix6 *prefix,
+ const struct in6_addr *addr)
+{
+ return ipv6_prefix_equal(&prefix->addr, addr, prefix->len);
+}
+
+static void ipxlat_rfc6052_46_embed(__be32 addr4, struct in6_addr *dst,
+ unsigned int q1, unsigned int q2,
+ unsigned int q3, unsigned int q4)
+{
+ const u32 src = ntohl(addr4);
+
+ dst->s6_addr[q1] = src >> 24;
+ dst->s6_addr[q2] = src >> 16;
+ dst->s6_addr[q3] = src >> 8;
+ dst->s6_addr[q4] = src;
+}
+
+static void ipxlat_rfc6052_46(const struct ipxlat_prefix6 *rfc6052_prefix6,
+ __be32 addr4, struct in6_addr *addr6)
+{
+ *addr6 = rfc6052_prefix6->addr;
+
+ /* always skip the 9th (index 8) byte which is the reserve "u" octet */
+ switch (rfc6052_prefix6->len) {
+ case 96:
+ addr6->s6_addr32[3] = addr4;
+ break;
+ case 64:
+ ipxlat_rfc6052_46_embed(addr4, addr6, 9, 10, 11, 12);
+ break;
+ case 56:
+ ipxlat_rfc6052_46_embed(addr4, addr6, 7, 9, 10, 11);
+ break;
+ case 48:
+ ipxlat_rfc6052_46_embed(addr4, addr6, 6, 7, 9, 10);
+ break;
+ case 40:
+ ipxlat_rfc6052_46_embed(addr4, addr6, 5, 6, 7, 9);
+ break;
+ case 32:
+ addr6->s6_addr32[1] = addr4;
+ break;
+ default:
+ DEBUG_NET_WARN_ON_ONCE(1);
+ break;
+ }
+}
+
+static __be32 ipxlat_rfc6052_64_extract(const struct in6_addr *src,
+ unsigned int q1, unsigned int q2,
+ unsigned int q3, unsigned int q4)
+{
+ const u32 a = src->s6_addr[q1];
+ const u32 b = src->s6_addr[q2];
+ const u32 c = src->s6_addr[q3];
+ const u32 d = src->s6_addr[q4];
+
+ return htonl((a << 24) | (b << 16) | (c << 8) | d);
+}
+
+static int ipxlat_rfc6052_64(const struct ipxlat_prefix6 *rfc6052_prefix6,
+ const struct in6_addr *addr6, __be32 *addr4)
+{
+ if (!ipxlat_prefix6_contains(rfc6052_prefix6, addr6) ||
+ addr6->s6_addr[8])
+ return -EINVAL;
+
+ /* always skip the 9th (index 8) byte which is the reserve "u" octet */
+ switch (rfc6052_prefix6->len) {
+ case 96:
+ *addr4 = addr6->s6_addr32[3];
+ break;
+ case 64:
+ *addr4 = ipxlat_rfc6052_64_extract(addr6, 9, 10, 11, 12);
+ break;
+ case 56:
+ *addr4 = ipxlat_rfc6052_64_extract(addr6, 7, 9, 10, 11);
+ break;
+ case 48:
+ *addr4 = ipxlat_rfc6052_64_extract(addr6, 6, 7, 9, 10);
+ break;
+ case 40:
+ *addr4 = ipxlat_rfc6052_64_extract(addr6, 5, 6, 7, 9);
+ break;
+ case 32:
+ *addr4 = addr6->s6_addr32[1];
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static void ipxlat_set_bit(struct in6_addr *addr, unsigned int bit, bool value)
+{
+ /* IPv6 prefix bits are numbered from the most significant bit */
+ u8 *p = &addr->s6_addr[bit >> 3];
+ u8 mask = BIT(7 - (bit & 7));
+
+ if (value)
+ *p |= mask;
+ else
+ *p &= ~mask;
+}
+
+static bool ipxlat_get_bit(const struct in6_addr *addr, unsigned int bit)
+{
+ return addr->s6_addr[bit >> 3] & BIT(7 - (bit & 7));
+}
+
+static void ipxlat_eam_46(__be32 addr4, const struct ipxlat_eam *eam,
+ struct in6_addr *addr6)
+{
+ const u8 host_bits = 32 - eam->prefix4.len;
+ const u32 host = ntohl(addr4);
+ unsigned int i;
+
+ /* append the IPv4 suffix to EAM6 and leave the unused tail zero */
+ *addr6 = eam->prefix6.addr;
+ for (i = 0; i < host_bits; ++i)
+ ipxlat_set_bit(addr6, eam->prefix6.len + i,
+ host & BIT(host_bits - 1 - i));
+}
+
+static void ipxlat_eam_64(const struct in6_addr *addr6,
+ const struct ipxlat_eam *eam, __be32 *addr4)
+{
+ const u8 host_bits = 32 - eam->prefix4.len;
+ unsigned int i;
+ u32 host = 0;
+
+ /* RFC 7757 discards IPv6 suffix bits which do not fit in IPv4 */
+ for (i = 0; i < host_bits; ++i)
+ host = (host << 1) |
+ ipxlat_get_bit(addr6, eam->prefix6.len + i);
+
+ *addr4 = eam->prefix4.addr | htonl(host);
+}
+
+static void ipxlat_46_convert_addr(const struct ipxlat_config *cfg,
+ __be32 addr4, struct in6_addr *addr6)
+{
+ const struct ipxlat_eam *eam = &cfg->eam;
+
+ if (cfg->has_eam && ipxlat_prefix4_contains(&eam->prefix4, addr4)) {
+ ipxlat_eam_46(addr4, eam, addr6);
+ return;
+ }
+
+ ipxlat_rfc6052_46(&cfg->rfc6052_prefix6, addr4, addr6);
+}
+
+static int ipxlat_64_convert_addr(const struct ipxlat_config *cfg,
+ const struct in6_addr *addr6, __be32 *addr4)
+{
+ const struct ipxlat_eam *eam = &cfg->eam;
+
+ if (cfg->has_eam && ipxlat_prefix6_contains(&eam->prefix6, addr6)) {
+ ipxlat_eam_64(addr6, eam, addr4);
+ return 0;
+ }
+
+ return ipxlat_rfc6052_64(&cfg->rfc6052_prefix6, addr6, addr4);
+}
+
+void ipxlat_46_convert_endpoints(const struct ipxlat_config *cfg,
+ const struct iphdr *iph4, struct in6_addr *src,
+ struct in6_addr *dst)
+{
+ ipxlat_46_convert_addr(cfg, iph4->saddr, src);
+ ipxlat_46_convert_addr(cfg, iph4->daddr, dst);
+}
+
+int ipxlat_64_convert_endpoints(const struct ipxlat_config *cfg,
+ const struct ipv6hdr *hdr6,
+ bool allow_dummy_src, __be32 *src, __be32 *dst)
+{
+ int err;
+
+ err = ipxlat_64_convert_addr(cfg, &hdr6->saddr, src);
+ if (err) {
+ if (!allow_dummy_src)
+ return err;
+
+ /* ICMPv6 errors may come from a router whose source address has
+ * no configured IPv4 mapping
+ */
+ *src = htonl(INADDR_DUMMY);
+ }
+
+ return ipxlat_64_convert_addr(cfg, &hdr6->daddr, dst);
+}
diff --git a/net/ipxlat/address.h b/net/ipxlat/address.h
new file mode 100644
index 000000000000..5f43c729bc94
--- /dev/null
+++ b/net/ipxlat/address.h
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#ifndef _NET_IPXLAT_ADDRESS_H_
+#define _NET_IPXLAT_ADDRESS_H_
+
+#include <linux/ip.h>
+#include <net/ipv6.h>
+
+#include <net/ipxlat.h>
+
+/**
+ * ipxlat_46_convert_endpoints - convert both endpoints of an IPv4 header
+ * @cfg: validated translation configuration
+ * @iph4: original IPv4 header
+ * @src: translated IPv6 source
+ * @dst: translated IPv6 destination
+ *
+ * A matching RFC 7757 EAM takes precedence for each endpoint. Endpoints not
+ * covered by the EAM are converted with the RFC 6052 prefix.
+ */
+void ipxlat_46_convert_endpoints(const struct ipxlat_config *cfg,
+ const struct iphdr *iph4, struct in6_addr *src,
+ struct in6_addr *dst);
+
+/**
+ * ipxlat_64_convert_endpoints - convert both endpoints of an IPv6 header
+ * @cfg: validated translation configuration
+ * @hdr6: original IPv6 header
+ * @allow_dummy_src: permit the RFC 7915 dummy IPv4 source (for an ICMP error)
+ * @src: translated IPv4 source
+ * @dst: translated IPv4 destination
+ *
+ * A matching RFC 7757 EAM takes precedence for each endpoint. Endpoints not
+ * covered by the EAM are converted with the RFC 6052 prefix.
+ *
+ * If the source address cannot be converted and @allow_dummy_src is true,
+ * INADDR_DUMMY will be used as source.
+ *
+ * Return: zero on success or a negative errno when an endpoint cannot be
+ * represented in IPv4.
+ */
+int ipxlat_64_convert_endpoints(const struct ipxlat_config *cfg,
+ const struct ipv6hdr *hdr6,
+ bool allow_dummy_src, __be32 *src, __be32 *dst);
+
+#endif /* _NET_IPXLAT_ADDRESS_H_ */
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 07/18] net: ipxlat: add packet metadata and header helpers
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (5 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 06/18] net: ipxlat: add RFC 6052 and EAM address translation Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 08/18] net: ipxlat: add IPv4 packet validation Ralf Lici
` (10 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Daniel Gröber, Antonio Quartulli,
linux-kernel
Define the skb control block used while a packet is prepared, together
with offset, length, fragment and pending-error helpers shared by
the two protocol families.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-4-ralf@mandelbit.com/
- Reshape the v1 control block for the prepare-translate engine contract,
remove netdevice actions and continuation policy from its metadata,
and split family-specific parsing into packet4.c and packet6.c.
- Make headers which expose IPv6 packet and Fragment Header types
include the declarations they use. (Sashiko)
net/ipxlat/packet.h | 243 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 243 insertions(+)
create mode 100644 net/ipxlat/packet.h
diff --git a/net/ipxlat/packet.h b/net/ipxlat/packet.h
new file mode 100644
index 000000000000..463f1ef027d0
--- /dev/null
+++ b/net/ipxlat/packet.h
@@ -0,0 +1,243 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#ifndef _NET_IPXLAT_PACKET_H_
+#define _NET_IPXLAT_PACKET_H_
+
+#include <net/ip.h>
+#include <net/ipv6.h>
+
+#include <net/ipxlat.h>
+
+/**
+ * struct ipxlat_cb - per-skb parser and control metadata stored in skb->cb
+ * @l4_off: outer L4 header offset
+ * @payload_off: outer payload offset
+ * @fragh_off: outer IPv6 Fragment Header offset, or 0 if absent
+ * @inner_l3_offset: quoted inner L3 offset for ICMP errors
+ * @inner_l4_offset: quoted inner L4 offset for ICMP errors
+ * @inner_fragh_off: quoted inner IPv6 Fragment Header offset, or 0
+ * @l4_proto: outer L4 protocol (or nexthdr for IPv6)
+ * @inner_l4_proto: quoted inner L4 protocol
+ * @l3_hdr_len: outer L3 header length including extension headers
+ * @is_icmp_err: packet is ICMP error and carries quoted inner packet
+ * @emit_icmp_err: datapath must emit translator-generated ICMP on drop
+ * @saddr4: translated outer IPv4 source address for 6->4
+ * @daddr4: translated outer IPv4 destination address for 6->4
+ * @inner_saddr4: translated quoted IPv4 source address for ICMP 6->4
+ * @inner_daddr4: translated quoted IPv4 destination address for ICMP 6->4
+ * @pending_icmp_err: ICMP type/code/info cached for deferred emission
+ * @pending_icmp_err.type: ICMP type to emit
+ * @pending_icmp_err.code: ICMP code to emit
+ * @pending_icmp_err.info: ICMP auxiliary info (e.g. pointer/MTU)
+ */
+struct ipxlat_cb {
+ u16 l4_off;
+ u16 payload_off;
+ u16 fragh_off;
+ u16 inner_l3_offset;
+ u16 inner_l4_offset;
+ u16 inner_fragh_off;
+ u16 l3_hdr_len;
+ u8 l4_proto;
+ u8 inner_l4_proto;
+ bool is_icmp_err;
+ bool emit_icmp_err;
+ __be32 saddr4;
+ __be32 daddr4;
+ __be32 inner_saddr4;
+ __be32 inner_daddr4;
+ struct {
+ u8 type;
+ u8 code;
+ u32 info;
+ } pending_icmp_err;
+};
+
+static inline struct ipxlat_cb *ipxlat_skb_cb(const struct sk_buff *skb)
+{
+ BUILD_BUG_ON(sizeof(struct ipxlat_cb) > sizeof(skb->cb));
+ return (struct ipxlat_cb *)(skb->cb);
+}
+
+static inline unsigned int ipxlat_skb_l4_len(const struct sk_buff *skb)
+{
+ return skb->len - skb_transport_offset(skb);
+}
+
+/* return the L4 length of one eventual segment rather than the GSO aggregate */
+static inline unsigned int ipxlat_skb_l4_seg_len(const struct sk_buff *skb,
+ const struct ipxlat_cb *cb)
+{
+ const struct skb_shared_info *shinfo;
+
+ if (skb_is_gso(skb)) {
+ shinfo = skb_shinfo(skb);
+ /* legacy UFO's gso_size already includes the UDP header */
+ if (shinfo->gso_type & SKB_GSO_UDP)
+ return shinfo->gso_size;
+
+ /* TCP and UDP-L4 GSO sizes describe payload bytes only */
+ return cb->payload_off - cb->l4_off + shinfo->gso_size;
+ }
+ return skb->len - cb->l4_off;
+}
+
+static inline u8 ipxlat_get_ipv6_tclass(const struct ipv6hdr *hdr)
+{
+ return (hdr->priority << 4) | (hdr->flow_lbl[0] >> 4);
+}
+
+static inline u16 ipxlat_get_frag6_offset(const struct frag_hdr *hdr)
+{
+ return be16_to_cpu(hdr->frag_off) & IP6_OFFSET;
+}
+
+static inline u16 ipxlat_get_frag4_offset(const struct iphdr *hdr)
+{
+ return (be16_to_cpu(hdr->frag_off) & IP_OFFSET) << 3;
+}
+
+static inline bool ipxlat_is_first_frag6(const struct frag_hdr *hdr)
+{
+ return !hdr || ipxlat_get_frag6_offset(hdr) == 0;
+}
+
+static inline bool ipxlat_is_first_frag4(const struct iphdr *hdr)
+{
+ return !(hdr->frag_off & htons(IP_OFFSET));
+}
+
+/**
+ * ipxlat_replace_l3 - replace an outer or quoted L3 header in place
+ * @skb: packet containing the L3 header
+ * @old_l3_end: current offset immediately after the L3 header
+ * @new_l3_end: desired offset immediately after the L3 header
+ * @transport_off: transport header offset after replacement
+ *
+ * The caller must provide valid offsets and enough headroom. Bytes following
+ * @old_l3_end remain unchanged, and the network header is reset to @skb->data.
+ */
+static inline void ipxlat_replace_l3(struct sk_buff *skb,
+ unsigned int old_l3_end,
+ unsigned int new_l3_end,
+ unsigned int transport_off)
+{
+ skb_pull(skb, old_l3_end);
+ skb_push(skb, new_l3_end);
+ skb_reset_network_header(skb);
+ skb_set_transport_header(skb, transport_off);
+}
+
+/**
+ * ipxlat_cb_rebase_offsets - shift cached cb offsets after skb relayout
+ * @cb: parsed packet metadata
+ * @delta: signed byte delta applied to cached offsets
+ *
+ * All cached absolute offsets include the old outer L3 length. Apply the same
+ * delta to keep them pointing at the same logical fields after replacement.
+ */
+static inline void ipxlat_cb_rebase_offsets(struct ipxlat_cb *cb, int delta)
+{
+ cb->l4_off += delta;
+ cb->payload_off += delta;
+
+ if (unlikely(cb->is_icmp_err)) {
+ cb->inner_l3_offset += delta;
+ cb->inner_l4_offset += delta;
+
+ if (cb->inner_fragh_off)
+ cb->inner_fragh_off += delta;
+ }
+}
+
+#ifdef CONFIG_DEBUG_NET
+/**
+ * ipxlat_cb_offsets_valid - validate monotonicity and bounds of cb offsets
+ * @cb: parsed packet metadata
+ *
+ * Checks that L4 precedes payload, quoted L3 follows the outer payload and a
+ * quoted Fragment Header lies inside the quoted L3 area before quoted L4.
+ *
+ * Return: true if cached offsets are internally consistent.
+ */
+static inline bool ipxlat_cb_offsets_valid(const struct ipxlat_cb *cb)
+{
+ if (unlikely(cb->payload_off < cb->l4_off))
+ return false;
+
+ if (cb->is_icmp_err) {
+ if (unlikely(cb->inner_l3_offset < cb->payload_off))
+ return false;
+ if (unlikely(cb->inner_l4_offset < cb->inner_l3_offset))
+ return false;
+ if (unlikely(cb->inner_fragh_off &&
+ cb->inner_fragh_off < cb->inner_l3_offset))
+ return false;
+ if (unlikely(cb->inner_fragh_off &&
+ cb->inner_fragh_off >= cb->inner_l4_offset))
+ return false;
+ }
+
+ return true;
+}
+#else
+static inline bool ipxlat_cb_offsets_valid(const struct ipxlat_cb *cb)
+{
+ return true;
+}
+#endif
+
+/**
+ * ipxlat_mark_icmp_drop - record an ICMP error for a dropped packet
+ * @skb: packet carrying the translator metadata
+ * @type: ICMP or ICMPv6 type
+ * @code: ICMP or ICMPv6 code
+ * @info: ICMP or ICMPv6 auxiliary information
+ */
+static inline void ipxlat_mark_icmp_drop(struct sk_buff *skb, u8 type, u8 code,
+ u32 info)
+{
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+
+ cb->emit_icmp_err = true;
+ cb->pending_icmp_err.type = type;
+ cb->pending_icmp_err.code = code;
+ cb->pending_icmp_err.info = info;
+}
+
+/**
+ * ipxlat_v4_validate_skb - validate and summarize IPv4 packet into skb->cb
+ * @skb: packet to validate
+ *
+ * Ensures required headers are present and consistent, populates
+ * &struct ipxlat_cb for subsequent 4->6 translation, and may mark a
+ * translator-generated ICMP action on failure paths.
+ *
+ * Return: 0 on success, negative errno on validation failure.
+ */
+int ipxlat_v4_validate_skb(struct sk_buff *skb);
+
+/**
+ * ipxlat_v6_validate_skb - validate and summarize IPv6 packet into skb->cb
+ * @skb: packet to validate
+ *
+ * Ensures required headers are present and consistent, populates
+ * &struct ipxlat_cb for subsequent 6->4 translation, and may mark a
+ * translator-generated ICMP action on failure paths.
+ *
+ * Return: 0 on success, negative errno on validation failure.
+ */
+int ipxlat_v6_validate_skb(struct sk_buff *skb);
+
+#endif /* _NET_IPXLAT_PACKET_H_ */
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 08/18] net: ipxlat: add IPv4 packet validation
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (6 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 07/18] net: ipxlat: add packet metadata and header helpers Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 09/18] net: ipxlat: add IPv6 " Ralf Lici
` (9 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Daniel Gröber, Antonio Quartulli,
linux-kernel
Validate IPv4 headers, options, transport headers, checksums and
bounded ICMP quotes while caching the offsets needed by later
translation stages.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-5-ralf@mandelbit.com/
- Split IPv4 parsing out of the v1 combined packet.c implementation.
- Separate unsupported packet shapes from malformed packets so an
attachment can fall back before translation starts.
- Bound quoted-packet parsing to the quoted datagram.
- Pull the complete declared outer TCP header before later stages use it.
- Treat a CHECKSUM_PARTIAL UDP packet as checksum-present even when the
checksum field still contains zero. (Sashiko)
- Validate ICMP checksums represented by CHECKSUM_COMPLETE rather than
trusting all hardware checksum metadata. (Sashiko)
net/ipxlat/Makefile | 1 +
net/ipxlat/packet4.c | 410 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 411 insertions(+)
create mode 100644 net/ipxlat/packet4.c
diff --git a/net/ipxlat/Makefile b/net/ipxlat/Makefile
index 013c338a2576..f517eaa75423 100644
--- a/net/ipxlat/Makefile
+++ b/net/ipxlat/Makefile
@@ -7,3 +7,4 @@ obj-$(CONFIG_NET_IPXLAT) += ipxlat.o
ipxlat-objs += config.o
ipxlat-objs += core.o
ipxlat-objs += address.o
+ipxlat-objs += packet4.o
diff --git a/net/ipxlat/packet4.c b/net/ipxlat/packet4.c
new file mode 100644
index 000000000000..590f95fb6b2c
--- /dev/null
+++ b/net/ipxlat/packet4.c
@@ -0,0 +1,410 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <linux/icmp.h>
+
+#include <net/ipxlat.h>
+
+#include "packet.h"
+
+static bool ipxlat_v4_validate_addr(__be32 addr4)
+{
+ return !(ipv4_is_zeronet(addr4) || ipv4_is_loopback(addr4) ||
+ ipv4_is_multicast(addr4) || ipv4_is_lbcast(addr4));
+}
+
+/* RFC 7915 Section 4.1 requires ignoring IPv4 options unless an unexpired
+ * LSRR/SSRR is present, in which case we must send ICMPv4 SR_FAILED.
+ * We intentionally treat malformed option encoding as invalid input and
+ * drop early instead of continuing translation.
+ */
+static int ipxlat_v4_srr_check(struct sk_buff *skb, const struct iphdr *hdr)
+{
+ const u8 *opt, *end;
+ u8 type, len, ptr;
+
+ if (likely(hdr->ihl <= 5))
+ return 0;
+
+ opt = (const u8 *)(hdr + 1);
+ end = (const u8 *)hdr + (hdr->ihl << 2);
+
+ while (opt < end) {
+ type = opt[0];
+ if (type == IPOPT_END)
+ return 0;
+ if (type == IPOPT_NOOP) {
+ opt++;
+ continue;
+ }
+
+ if (unlikely(end - opt < 2))
+ return -EINVAL;
+
+ len = opt[1];
+ if (unlikely(len < 2 || opt + len > end))
+ return -EINVAL;
+
+ if (type == IPOPT_LSRR || type == IPOPT_SSRR) {
+ if (unlikely(len < 3))
+ return -EINVAL;
+
+ /* points to the beginning of the next IP addr */
+ ptr = opt[2];
+ if (unlikely(ptr < 4))
+ return -EINVAL;
+ if (unlikely(ptr > len))
+ return 0;
+ if (unlikely(ptr > len - 3))
+ return -EINVAL;
+
+ ipxlat_mark_icmp_drop(skb, ICMP_DEST_UNREACH,
+ ICMP_SR_FAILED, 0);
+ return -EINVAL;
+ }
+
+ opt += len;
+ }
+
+ return 0;
+}
+
+static int ipxlat_v4_pull_l3(struct sk_buff *skb, unsigned int l3_offset,
+ unsigned int packet_end)
+{
+ const struct iphdr *iph;
+ unsigned int tot_len;
+ int l3_len;
+
+ if (unlikely(l3_offset > packet_end ||
+ sizeof(*iph) > packet_end - l3_offset ||
+ !pskb_may_pull(skb, l3_offset + sizeof(*iph))))
+ return -EINVAL;
+
+ iph = (const struct iphdr *)(skb->data + l3_offset);
+ if (unlikely(iph->version != 4 || iph->ihl < 5))
+ return -EINVAL;
+
+ l3_len = iph->ihl << 2;
+ if (unlikely(l3_len > packet_end - l3_offset))
+ return -EINVAL;
+
+ /* an ICMP skb has no GSO representation, so iph_totlen also works
+ * for quoted inner headers without using unrelated GSO metadata
+ */
+ tot_len = iph_totlen(skb, iph);
+ if (unlikely(tot_len < l3_len))
+ return -EINVAL;
+
+ if (unlikely(!pskb_may_pull(skb, l3_offset + l3_len)))
+ return -EINVAL;
+
+ return l3_len;
+}
+
+static int ipxlat_v4_pull_l4(struct sk_buff *skb, unsigned int l4_offset,
+ u8 l4_proto, bool *is_icmp_err)
+{
+ unsigned int tcp_hdr_len;
+ struct icmphdr *icmp;
+ struct udphdr *udp;
+ struct tcphdr *tcp;
+
+ *is_icmp_err = false;
+
+ switch (l4_proto) {
+ case IPPROTO_TCP:
+ if (unlikely(!pskb_may_pull(skb, l4_offset + sizeof(*tcp))))
+ return -EINVAL;
+
+ tcp = (struct tcphdr *)(skb->data + l4_offset);
+ if (unlikely(tcp->doff < 5))
+ return -EINVAL;
+
+ tcp_hdr_len = __tcp_hdrlen(tcp);
+ if (unlikely(!pskb_may_pull(skb, l4_offset + tcp_hdr_len)))
+ return -EINVAL;
+
+ return tcp_hdr_len;
+ case IPPROTO_UDP:
+ if (unlikely(!pskb_may_pull(skb, l4_offset + sizeof(*udp))))
+ return -EINVAL;
+
+ udp = (struct udphdr *)(skb->data + l4_offset);
+ if (unlikely(ntohs(udp->len) < sizeof(*udp)))
+ return -EINVAL;
+
+ return sizeof(struct udphdr);
+ case IPPROTO_ICMP:
+ if (unlikely(!pskb_may_pull(skb, l4_offset + sizeof(*icmp))))
+ return -EINVAL;
+
+ icmp = (struct icmphdr *)(skb->data + l4_offset);
+ *is_icmp_err = icmp_is_err(icmp->type);
+ return sizeof(struct icmphdr);
+ default:
+ return 0;
+ }
+}
+
+/* ICMP errors are only required to quote the first eight transport bytes.
+ * That is enough for UDP and ICMP, but not for a TCP data offset/checksum.
+ * We leave a quoted TCP header unchanged when the quote does not contain the
+ * complete header.
+ */
+static int ipxlat_v4_pull_icmp_l4(struct sk_buff *skb, unsigned int l4_offset,
+ unsigned int quote_end, u8 l4_proto)
+{
+ struct icmphdr *icmp;
+ unsigned int need;
+
+ switch (l4_proto) {
+ case IPPROTO_ICMP:
+ need = sizeof(*icmp);
+ break;
+ case IPPROTO_UDP:
+ need = sizeof(struct udphdr);
+ break;
+ default:
+ return 0;
+ }
+
+ if (unlikely(l4_offset > quote_end || need > quote_end - l4_offset ||
+ !pskb_may_pull(skb, l4_offset + need)))
+ return -EINVAL;
+
+ if (l4_proto == IPPROTO_ICMP) {
+ icmp = (struct icmphdr *)(skb->data + l4_offset);
+ if (unlikely(icmp_is_err(icmp->type)))
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ipxlat_v4_icmp_quote_end(const struct sk_buff *skb,
+ unsigned int payload_off,
+ unsigned int *quote_end)
+{
+ const struct icmphdr *icmp =
+ (const struct icmphdr *)(skb->data + payload_off -
+ sizeof(*icmp));
+ unsigned int quote_len = icmp->un.reserved[1] << 2;
+
+ if (!quote_len) {
+ *quote_end = skb->len;
+ return 0;
+ }
+
+ /* a nonzero RFC 4884 delimiter describes at least 128 quoted bytes */
+ if (unlikely(quote_len < ICMP_EXT_ORIG_DGRAM_MIN_LEN ||
+ payload_off > skb->len ||
+ quote_len > skb->len - payload_off))
+ return -EINVAL;
+
+ *quote_end = payload_off + quote_len;
+ return 0;
+}
+
+static int ipxlat_v4_pull_icmp_inner(struct sk_buff *skb,
+ unsigned int inner_l3_off,
+ unsigned int quote_end)
+{
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ unsigned int datagram_end, inner_l4_off;
+ const struct iphdr *inner_l3_hdr;
+ int inner_l3_len, err;
+
+ inner_l3_len = ipxlat_v4_pull_l3(skb, inner_l3_off, quote_end);
+ if (unlikely(inner_l3_len < 0))
+ return inner_l3_len;
+
+ inner_l3_hdr = (const struct iphdr *)(skb->data + inner_l3_off);
+ datagram_end = inner_l3_off + ntohs(inner_l3_hdr->tot_len);
+
+ /* accept non-first quoted fragments: only inner L3 is translatable */
+ inner_l4_off = inner_l3_off + inner_l3_len;
+ if (unlikely(inner_l3_off > U16_MAX || inner_l4_off > U16_MAX))
+ return -E2BIG;
+ cb->inner_l3_offset = inner_l3_off;
+ cb->inner_l4_offset = inner_l4_off;
+
+ if (unlikely(!ipxlat_is_first_frag4(inner_l3_hdr)))
+ return 0;
+
+ err = ipxlat_v4_pull_icmp_l4(skb, inner_l4_off,
+ min(quote_end, datagram_end),
+ inner_l3_hdr->protocol);
+ if (unlikely(err < 0))
+ return err;
+
+ return 0;
+}
+
+static int ipxlat_v4_pull_hdrs(struct sk_buff *skb)
+{
+ const unsigned int l3_off = skb_network_offset(skb);
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ unsigned int payload_off, quote_end;
+ const struct iphdr *l3_hdr;
+ int err, l3_len, l4_len;
+
+ /* parse IPv4 header and get its full length including options */
+ l3_len = ipxlat_v4_pull_l3(skb, l3_off, skb->len);
+ if (unlikely(l3_len < 0))
+ return l3_len;
+ l3_hdr = ip_hdr(skb);
+
+ if (unlikely(!ipxlat_v4_validate_addr(l3_hdr->daddr)))
+ return -EINVAL;
+
+ /* RFC 7915 Section 4.1 */
+ if (unlikely(ipxlat_v4_srr_check(skb, l3_hdr)))
+ return -EINVAL;
+ /* fragmented ICMPs cannot be translated by a stateless translator */
+ if (unlikely(l3_hdr->protocol == IPPROTO_ICMP &&
+ ip_is_fragment(l3_hdr)))
+ return -EINVAL;
+
+ if (unlikely(l3_off + l3_len > U16_MAX))
+ return -E2BIG;
+
+ cb->l3_hdr_len = l3_len;
+ cb->l4_proto = l3_hdr->protocol;
+ cb->l4_off = l3_off + l3_len;
+ cb->payload_off = cb->l4_off;
+ cb->is_icmp_err = false;
+
+ /* only non fragmented packets or first fragments have transport hdrs */
+ if (unlikely(!ipxlat_is_first_frag4(l3_hdr))) {
+ if (unlikely(!ipxlat_v4_validate_addr(l3_hdr->saddr)))
+ return -EINVAL;
+ return 0;
+ }
+
+ /* parse transport header and get its full length */
+ l4_len = ipxlat_v4_pull_l4(skb, cb->l4_off, l3_hdr->protocol,
+ &cb->is_icmp_err);
+ if (unlikely(l4_len < 0))
+ return l4_len;
+ l3_hdr = ip_hdr(skb);
+
+ /* illegal IPv4 sources are accepted only for ICMPv4 errors */
+ if (unlikely(!ipxlat_v4_validate_addr(l3_hdr->saddr) &&
+ !cb->is_icmp_err))
+ return -EINVAL;
+
+ payload_off = cb->l4_off + l4_len;
+ if (unlikely(payload_off > U16_MAX))
+ return -E2BIG;
+ cb->payload_off = payload_off;
+
+ /* validate the quoted packet if it's an ICMP error */
+ if (unlikely(cb->is_icmp_err)) {
+ DEBUG_NET_WARN_ON_ONCE(cb->l4_proto != IPPROTO_ICMP);
+
+ err = ipxlat_v4_icmp_quote_end(skb, cb->payload_off,
+ "e_end);
+ if (unlikely(err))
+ return err;
+
+ err = ipxlat_v4_pull_icmp_inner(skb, cb->payload_off,
+ quote_end);
+ if (unlikely(err))
+ return err;
+ }
+
+ return 0;
+}
+
+static int ipxlat_v4_validate_icmp_csum(const struct sk_buff *skb,
+ unsigned int l4_off)
+{
+ __sum16 csum;
+
+ switch (skb->ip_summed) {
+ case CHECKSUM_UNNECESSARY:
+ case CHECKSUM_PARTIAL:
+ return 0;
+ case CHECKSUM_COMPLETE:
+ return csum_fold(skb->csum) ? -EINVAL : 0;
+ case CHECKSUM_NONE:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* compute checksum over ICMP header and payload, then fold to 16-bit
+ * Internet checksum to validate it
+ */
+ csum = csum_fold(skb_checksum(skb, l4_off, skb->len - l4_off, 0));
+ return unlikely(csum) ? -EINVAL : 0;
+}
+
+int ipxlat_v4_validate_skb(struct sk_buff *skb)
+{
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ unsigned int udp_len;
+ struct iphdr *l3_hdr;
+ struct udphdr *udph;
+ int err;
+
+ err = ipxlat_v4_pull_hdrs(skb);
+ if (unlikely(err))
+ return err;
+
+ if (unlikely(cb->is_icmp_err)) {
+ /* Translation path recomputes ICMPv6 checksum from scratch.
+ * Validate here so a corrupted ICMPv4 error is not converted
+ * into a translated packet with a valid checksum.
+ */
+ return ipxlat_v4_validate_icmp_csum(skb, cb->l4_off);
+ }
+
+ l3_hdr = ip_hdr(skb);
+ if (unlikely(!ipxlat_is_first_frag4(l3_hdr)))
+ return 0;
+
+ if (cb->l4_proto != IPPROTO_UDP)
+ return 0;
+
+ udph = (struct udphdr *)(skb->data + cb->l4_off);
+ /* CHECKSUM_PARTIAL is authoritative even when the checksum field is
+ * zero, translation will simply reseed it for the IPv6 pseudo-header
+ */
+ if (skb->ip_summed == CHECKSUM_PARTIAL)
+ return 0;
+
+ if (likely(udph->check != 0))
+ return 0;
+
+ /* We are in the path where the UDP header is present (unfragmented
+ * packets or first fragments) and its checksum is zero. Fragmented
+ * checksum-less IPv4 UDP is rejected because 4->6 cannot reliably
+ * translate it.
+ * We record an administrative-prohibited error so an ingress harness
+ * can tell the sender why it was dropped.
+ */
+ if (unlikely(ip_is_fragment(l3_hdr))) {
+ ipxlat_mark_icmp_drop(skb, ICMP_DEST_UNREACH, ICMP_PKT_FILTERED,
+ 0);
+ return -EINVAL;
+ }
+
+ /* udph->len bounds the span used to compute replacement checksum */
+ udp_len = ntohs(udph->len);
+ if (unlikely(udp_len > skb->len - cb->l4_off))
+ return -EINVAL;
+
+ return 0;
+}
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 09/18] net: ipxlat: add IPv6 packet validation
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (7 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 08/18] net: ipxlat: add IPv4 packet validation Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 10/18] net: ipxlat: add checksum and offload helpers Ralf Lici
` (8 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Daniel Gröber, Antonio Quartulli,
Xavier Hsinyuan, linux-kernel
Validate IPv6 base and extension headers, transport headers, checksums
and bounded ICMPv6 quotes while recording the headers that survive
translation.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-6-ralf@mandelbit.com/
- Split IPv6 parsing out of the v1 combined packet.c implementation.
- Replace the IPv6 header walk with one explicitly bounded by the packet
or quoted-datagram length.
- Include the IPv6 checksum declarations used by the ICMPv6 validator.
(Xavier)
- Validate the TCP data offset and pull the complete declared header
before later translation stages use it. (Sashiko)
- Accept a zero IPv6 payload length for the kernel's oversized GSO
representation instead of rejecting it unconditionally. (Sashiko)
- Validate ICMPv6 checksums represented by CHECKSUM_COMPLETE. (Sashiko)
- Stop IPv6 header walking at an Authentication Header instead of
silently removing it from the translated packet.
- Reject a zero UDPv6 checksum unless CHECKSUM_PARTIAL indicates that
checksum completion is still pending.
net/ipxlat/Makefile | 1 +
net/ipxlat/packet6.c | 468 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 469 insertions(+)
create mode 100644 net/ipxlat/packet6.c
diff --git a/net/ipxlat/Makefile b/net/ipxlat/Makefile
index f517eaa75423..56c8a78fc863 100644
--- a/net/ipxlat/Makefile
+++ b/net/ipxlat/Makefile
@@ -8,3 +8,4 @@ ipxlat-objs += config.o
ipxlat-objs += core.o
ipxlat-objs += address.o
ipxlat-objs += packet4.o
+ipxlat-objs += packet6.o
diff --git a/net/ipxlat/packet6.c b/net/ipxlat/packet6.c
new file mode 100644
index 000000000000..c772a2967b30
--- /dev/null
+++ b/net/ipxlat/packet6.c
@@ -0,0 +1,468 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <linux/icmp.h>
+#include <linux/icmpv6.h>
+
+#include <net/ip6_checksum.h>
+#include <net/ipxlat.h>
+
+#include "packet.h"
+
+static bool ipxlat_v6_validate_saddr(const struct in6_addr *addr6)
+{
+ return !(ipv6_addr_any(addr6) || ipv6_addr_loopback(addr6) ||
+ ipv6_addr_is_multicast(addr6));
+}
+
+static int ipxlat_v6_pull_l4(struct sk_buff *skb, unsigned int l4_offset,
+ u8 l4_proto, bool *is_icmp_err)
+{
+ unsigned int tcp_hdr_len;
+ struct icmp6hdr *icmp;
+ struct udphdr *udp;
+ struct tcphdr *tcp;
+
+ *is_icmp_err = false;
+
+ switch (l4_proto) {
+ case NEXTHDR_TCP:
+ if (unlikely(!pskb_may_pull(skb, l4_offset + sizeof(*tcp))))
+ return -EINVAL;
+
+ tcp = (struct tcphdr *)(skb->data + l4_offset);
+ if (unlikely(tcp->doff < 5))
+ return -EINVAL;
+
+ tcp_hdr_len = __tcp_hdrlen(tcp);
+ if (unlikely(!pskb_may_pull(skb, l4_offset + tcp_hdr_len)))
+ return -EINVAL;
+
+ return tcp_hdr_len;
+ case NEXTHDR_UDP:
+ if (unlikely(!pskb_may_pull(skb, l4_offset + sizeof(*udp))))
+ return -EINVAL;
+
+ udp = (struct udphdr *)(skb->data + l4_offset);
+ if (unlikely(ntohs(udp->len) < sizeof(*udp)))
+ return -EINVAL;
+
+ return sizeof(struct udphdr);
+ case NEXTHDR_ICMP:
+ if (unlikely(!pskb_may_pull(skb, l4_offset + sizeof(*icmp))))
+ return -EINVAL;
+
+ icmp = (struct icmp6hdr *)(skb->data + l4_offset);
+ *is_icmp_err = icmpv6_is_err(icmp->icmp6_type);
+ return sizeof(struct icmp6hdr);
+ default:
+ return 0;
+ }
+}
+
+/* see ipxlat_v4_pull_icmp_l4: a minimum ICMP quote need not contain a
+ * complete TCP header, so only inspect fields present in the first 8 bytes
+ */
+static int ipxlat_v6_pull_icmp_l4(struct sk_buff *skb, unsigned int l4_offset,
+ unsigned int quote_end, u8 l4_proto)
+{
+ struct icmp6hdr *icmp;
+ unsigned int need;
+
+ switch (l4_proto) {
+ case NEXTHDR_ICMP:
+ need = sizeof(*icmp);
+ break;
+ case NEXTHDR_UDP:
+ need = sizeof(struct udphdr);
+ break;
+ default:
+ return 0;
+ }
+
+ if (unlikely(l4_offset > quote_end || need > quote_end - l4_offset ||
+ !pskb_may_pull(skb, l4_offset + need)))
+ return -EINVAL;
+
+ if (l4_proto == NEXTHDR_ICMP) {
+ icmp = (struct icmp6hdr *)(skb->data + l4_offset);
+ if (unlikely(icmpv6_is_err(icmp->icmp6_type)))
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ipxlat_v6_icmp_quote_end(const struct sk_buff *skb,
+ unsigned int payload_off,
+ unsigned int *quote_end)
+{
+ const struct icmp6hdr *icmp;
+ unsigned int quote_len;
+
+ icmp = (const struct icmp6hdr *)(skb->data + payload_off -
+ sizeof(*icmp));
+ quote_len = icmp->icmp6_datagram_len << 3;
+ if (!quote_len) {
+ *quote_end = skb->len;
+ return 0;
+ }
+
+ /* a nonzero RFC 4884 delimiter describes at least 128 quoted bytes */
+ if (unlikely(quote_len < ICMP_EXT_ORIG_DGRAM_MIN_LEN ||
+ payload_off > skb->len ||
+ quote_len > skb->len - payload_off))
+ return -EINVAL;
+
+ *quote_end = payload_off + quote_len;
+ return 0;
+}
+
+/* Walk one IPv6 header chain without reading past packet_end. Outer packets
+ * also reject an active Routing Header; quoted packets are parsed only far
+ * enough to translate the bytes they contain.
+ */
+struct ipxlat_v6_hdrs {
+ unsigned int fragh_off;
+ unsigned int l4_off;
+ u8 l4_proto;
+ bool has_l4;
+};
+
+static bool ipxlat_v6_may_pull(struct sk_buff *skb, unsigned int offset,
+ unsigned int len, unsigned int packet_end)
+{
+ if (offset > packet_end || len > packet_end - offset)
+ return false;
+ return pskb_may_pull(skb, offset + len);
+}
+
+static int ipxlat_v6_walk_hdrs(struct sk_buff *skb, unsigned int l3_offset,
+ unsigned int packet_end,
+ struct ipxlat_v6_hdrs *hdrs,
+ bool check_rh)
+{
+ unsigned int ext_len, off;
+ struct ipv6_opt_hdr *ext;
+ struct ipv6_rt_hdr *rh;
+ struct frag_hdr *frag;
+ struct ipv6hdr *ip6h;
+ u8 nexthdr;
+ u32 ptr;
+
+ if (unlikely(!ipxlat_v6_may_pull(skb, l3_offset, sizeof(*ip6h),
+ packet_end)))
+ return -EINVAL;
+
+ ip6h = (struct ipv6hdr *)(skb->data + l3_offset);
+ nexthdr = ip6h->nexthdr;
+ off = l3_offset + sizeof(*ip6h);
+ hdrs->has_l4 = true;
+ hdrs->fragh_off = 0;
+
+ while (ipv6_ext_hdr(nexthdr)) {
+ /* AH is protected payload here: translation preserves it but
+ * does not inspect or rewrite anything after it
+ */
+ if (nexthdr == NEXTHDR_AUTH || nexthdr == NEXTHDR_NONE)
+ break;
+
+ if (nexthdr == NEXTHDR_FRAGMENT) {
+ if (unlikely(hdrs->fragh_off ||
+ !ipxlat_v6_may_pull(skb, off,
+ sizeof(*frag),
+ packet_end)))
+ return -EINVAL;
+
+ frag = (struct frag_hdr *)(skb->data + off);
+ hdrs->fragh_off = off;
+ nexthdr = frag->nexthdr;
+ off += sizeof(*frag);
+
+ /* IPv6 fragment offsets include any extension headers
+ * after the Fragment Header. Later fragments do not
+ * carry those headers, so a stateless translator
+ * cannot remove them and adjust every offset. AH also
+ * cannot survive the outer header rewrite because the
+ * translator cannot update its integrity value.
+ * ipv6_ext_hdr deliberately excludes ESP, which is
+ * preserved as opaque payload as required by RFC 7915.
+ */
+ if (unlikely(ipv6_ext_hdr(nexthdr) &&
+ nexthdr != NEXTHDR_NONE)) {
+ ipxlat_mark_icmp_drop(skb, ICMPV6_DEST_UNREACH,
+ ICMPV6_ADM_PROHIBITED, 0);
+ return -EINVAL;
+ }
+
+ if (!ipxlat_is_first_frag6(frag)) {
+ hdrs->l4_proto = nexthdr;
+ hdrs->l4_off = off;
+ hdrs->has_l4 = false;
+ return 0;
+ }
+ continue;
+ }
+
+ if (unlikely(!ipxlat_v6_may_pull(skb, off, sizeof(*ext),
+ packet_end)))
+ return -EINVAL;
+
+ ext = (struct ipv6_opt_hdr *)(skb->data + off);
+ ext_len = ipv6_optlen(ext);
+ if (unlikely(!ipxlat_v6_may_pull(skb, off, ext_len,
+ packet_end)))
+ return -EINVAL;
+
+ ext = (struct ipv6_opt_hdr *)(skb->data + off);
+ if (check_rh && nexthdr == NEXTHDR_ROUTING) {
+ rh = (struct ipv6_rt_hdr *)ext;
+ ptr = off - l3_offset +
+ offsetof(struct ipv6_rt_hdr, segments_left);
+
+ if (unlikely(rh->segments_left)) {
+ ipxlat_mark_icmp_drop(skb, ICMPV6_PARAMPROB,
+ ICMPV6_HDR_FIELD, ptr);
+ return -EINVAL;
+ }
+ }
+
+ nexthdr = ext->nexthdr;
+ off += ext_len;
+ }
+
+ hdrs->l4_proto = nexthdr;
+ hdrs->l4_off = off;
+ return 0;
+}
+
+static int ipxlat_v6_pull_outer_l3(struct sk_buff *skb)
+{
+ const unsigned int l3_off = skb_network_offset(skb);
+ unsigned int payload_len, skb_payload_len;
+ struct ipv6hdr *l3_hdr;
+
+ if (unlikely(!pskb_may_pull(skb, l3_off + sizeof(*l3_hdr))))
+ return -EINVAL;
+ l3_hdr = ipv6_hdr(skb);
+
+ if (unlikely(l3_hdr->version != 6 ||
+ !ipxlat_v6_validate_saddr(&l3_hdr->saddr)))
+ return -EINVAL;
+
+ payload_len = be16_to_cpu(l3_hdr->payload_len);
+ skb_payload_len = skb->len - l3_off - sizeof(*l3_hdr);
+ /* A zero payload length represents an empty packet, a wire jumbogram,
+ * or an oversized GSO aggregate. Jumbograms are not supported.
+ */
+ if (payload_len) {
+ if (unlikely(payload_len != skb_payload_len))
+ return -EINVAL;
+ return 0;
+ }
+ if (!skb_payload_len)
+ return 0;
+ if (unlikely(!skb_is_gso(skb) || skb_payload_len <= IPV6_MAXPLEN))
+ return -EINVAL;
+
+ return 0;
+}
+
+static int ipxlat_v6_pull_icmp_inner(struct sk_buff *skb,
+ unsigned int outer_payload_off,
+ unsigned int quote_end)
+{
+ struct ipxlat_v6_hdrs hdrs;
+ unsigned int datagram_end, inner_l3_len;
+ unsigned int inner_payload_len;
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ struct ipv6hdr *inner_ip6;
+ int err;
+
+ if (unlikely(!ipxlat_v6_may_pull(skb, outer_payload_off,
+ sizeof(*inner_ip6), quote_end)))
+ return -EINVAL;
+
+ inner_ip6 = (struct ipv6hdr *)(skb->data + outer_payload_off);
+ if (unlikely(inner_ip6->version != 6))
+ return -EINVAL;
+ inner_payload_len = be16_to_cpu(inner_ip6->payload_len);
+ datagram_end = outer_payload_off + sizeof(*inner_ip6) +
+ inner_payload_len;
+ quote_end = min(quote_end, datagram_end);
+
+ err = ipxlat_v6_walk_hdrs(skb, outer_payload_off, quote_end, &hdrs,
+ false);
+ if (unlikely(err))
+ return err;
+ inner_l3_len = hdrs.l4_off - outer_payload_off;
+ if (unlikely(inner_l3_len > sizeof(*inner_ip6) + inner_payload_len))
+ return -EINVAL;
+ if (unlikely(outer_payload_off > U16_MAX || hdrs.l4_off > U16_MAX ||
+ hdrs.fragh_off > U16_MAX))
+ return -E2BIG;
+
+ cb->inner_l3_offset = outer_payload_off;
+ cb->inner_l4_offset = hdrs.l4_off;
+ cb->inner_fragh_off = hdrs.fragh_off;
+ cb->inner_l4_proto = hdrs.l4_proto;
+
+ if (likely(hdrs.has_l4)) {
+ err = ipxlat_v6_pull_icmp_l4(skb, hdrs.l4_off, quote_end,
+ hdrs.l4_proto);
+ if (unlikely(err < 0))
+ return err;
+ }
+
+ return 0;
+}
+
+static int ipxlat_v6_pull_hdrs(struct sk_buff *skb)
+{
+ const unsigned int l3_off = skb_network_offset(skb);
+ unsigned int l3_len, payload_off, quote_end;
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ struct ipxlat_v6_hdrs hdrs;
+ struct frag_hdr *frag;
+ int l4_len, err;
+
+ /* parse IPv6 base header and perform basic structural checks */
+ err = ipxlat_v6_pull_outer_l3(skb);
+ if (unlikely(err))
+ return err;
+
+ /* walk extension/fragment headers and locate the transport header */
+ err = ipxlat_v6_walk_hdrs(skb, l3_off, skb->len, &hdrs, true);
+ if (unlikely(err))
+ return err;
+
+ l3_len = hdrs.l4_off - l3_off;
+ payload_off = hdrs.l4_off;
+ if (unlikely(hdrs.l4_off > U16_MAX || hdrs.fragh_off > U16_MAX ||
+ l3_len > U16_MAX))
+ return -E2BIG;
+
+ cb->l4_proto = hdrs.l4_proto;
+ cb->l4_off = hdrs.l4_off;
+ cb->fragh_off = hdrs.fragh_off;
+ cb->l3_hdr_len = l3_len;
+
+ if (likely(hdrs.has_l4)) {
+ l4_len = ipxlat_v6_pull_l4(skb, hdrs.l4_off, hdrs.l4_proto,
+ &cb->is_icmp_err);
+ if (unlikely(l4_len < 0))
+ return l4_len;
+ payload_off += l4_len;
+ }
+ if (unlikely(payload_off > U16_MAX))
+ return -E2BIG;
+ cb->payload_off = payload_off;
+
+ if (unlikely(hdrs.l4_proto == NEXTHDR_ICMP && hdrs.fragh_off)) {
+ /* A stateless translator cannot reliably translate ICMP
+ * checksum across real IPv6 fragments, so fragmented ICMP is
+ * dropped. A Fragment Header alone, however, is not enough to
+ * decide: so-called atomic fragments (offset=0, M=0) carry a
+ * Fragment Header but are not actually fragmented.
+ */
+ frag = (struct frag_hdr *)(skb->data + hdrs.fragh_off);
+ if (unlikely(ipxlat_get_frag6_offset(frag) ||
+ (be16_to_cpu(frag->frag_off) & IP6_MF)))
+ return -EINVAL;
+ }
+
+ if (unlikely(cb->is_icmp_err)) {
+ /* validate the quoted packet in an ICMP error */
+ err = ipxlat_v6_icmp_quote_end(skb, payload_off, "e_end);
+ if (unlikely(err))
+ return err;
+
+ err = ipxlat_v6_pull_icmp_inner(skb, payload_off, quote_end);
+ if (unlikely(err))
+ return err;
+ }
+
+ DEBUG_NET_WARN_ON_ONCE(cb->is_icmp_err && cb->l4_proto != NEXTHDR_ICMP);
+
+ return 0;
+}
+
+static int ipxlat_v6_validate_icmp_csum(const struct sk_buff *skb,
+ unsigned int l4_off)
+{
+ struct ipv6hdr *iph6;
+ unsigned int len;
+ __sum16 csum;
+
+ switch (skb->ip_summed) {
+ case CHECKSUM_UNNECESSARY:
+ case CHECKSUM_PARTIAL:
+ return 0;
+ case CHECKSUM_COMPLETE:
+ iph6 = ipv6_hdr(skb);
+ len = skb->len - l4_off;
+ if (csum_ipv6_magic(&iph6->saddr, &iph6->daddr, len,
+ NEXTHDR_ICMP, skb->csum))
+ return -EINVAL;
+ return 0;
+ case CHECKSUM_NONE:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ iph6 = ipv6_hdr(skb);
+ len = skb->len - l4_off;
+ csum = csum_ipv6_magic(&iph6->saddr, &iph6->daddr, len, NEXTHDR_ICMP,
+ skb_checksum(skb, l4_off, len, 0));
+
+ return unlikely(csum) ? -EINVAL : 0;
+}
+
+int ipxlat_v6_validate_skb(struct sk_buff *skb)
+{
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ unsigned int l3_off, max_len6, pkt_len4, pkt_len6;
+ int err;
+
+ err = ipxlat_v6_pull_hdrs(skb);
+ if (unlikely(err))
+ return err;
+
+ /* IPv6's payload length does not include its 40-byte base header, so a
+ * legal IPv6 packet can be 65575 bytes long. IPv4's total length does
+ * include its header, and zero is only meaningful for a GSO aggregate.
+ */
+ l3_off = skb_network_offset(skb);
+ pkt_len6 = skb->len - l3_off;
+ pkt_len4 = pkt_len6 - cb->l3_hdr_len + sizeof(struct iphdr);
+ if (unlikely(!skb_is_gso(skb) && pkt_len4 > IP_MAX_MTU)) {
+ /* IPv4 cannot represent the translated length: tell a remote
+ * IPv6 sender the largest packet that can cross this node.
+ */
+ max_len6 = IP_MAX_MTU + cb->l3_hdr_len - sizeof(struct iphdr);
+ ipxlat_mark_icmp_drop(skb, ICMPV6_PKT_TOOBIG, 0, max_len6);
+ return -EMSGSIZE;
+ }
+
+ if (unlikely(cb->is_icmp_err)) {
+ /* the translated ICMPv4 checksum is recomputed from scratch,
+ * so reject bad ICMPv6 error checksums before conversion
+ */
+ err = ipxlat_v6_validate_icmp_csum(skb, cb->l4_off);
+ if (unlikely(err))
+ return err;
+ }
+
+ return 0;
+}
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 10/18] net: ipxlat: add checksum and offload helpers
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (8 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 09/18] net: ipxlat: add IPv6 " Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 11/18] net: ipxlat: add IPv4/IPv6 header and transport translation Ralf Lici
` (7 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Daniel Gröber, Antonio Quartulli,
linux-kernel
Add incremental pseudo-header checksum replacement, CHECKSUM_PARTIAL
setup, complete IPv6 checksums, RFC 4884 payload relayout and
cross-family GSO metadata normalization.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-7-ralf@mandelbit.com/
- Rework the v1 checksum helpers around the attachment-independent
engine.
- Distinguish GSO aggregate length from eventual segment length,
including legacy UFO's header-inclusive gso_size convention.
- Introduce helpers only in the patch containing their first caller,
keeping each intermediate build free of unused-static-function
warnings. (Sashiko)
net/ipxlat/Makefile | 1 +
net/ipxlat/transport.c | 179 +++++++++++++++++++++++++++++++++++++++++
net/ipxlat/transport.h | 127 +++++++++++++++++++++++++++++
3 files changed, 307 insertions(+)
create mode 100644 net/ipxlat/transport.c
create mode 100644 net/ipxlat/transport.h
diff --git a/net/ipxlat/Makefile b/net/ipxlat/Makefile
index 56c8a78fc863..91baab41d5eb 100644
--- a/net/ipxlat/Makefile
+++ b/net/ipxlat/Makefile
@@ -9,3 +9,4 @@ ipxlat-objs += core.o
ipxlat-objs += address.o
ipxlat-objs += packet4.o
ipxlat-objs += packet6.o
+ipxlat-objs += transport.o
diff --git a/net/ipxlat/transport.c b/net/ipxlat/transport.c
new file mode 100644
index 000000000000..118f5801c7e8
--- /dev/null
+++ b/net/ipxlat/transport.c
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <net/ip.h>
+#include <net/ip6_checksum.h>
+
+#include "transport.h"
+
+int ipxlat_set_partial_csum(struct sk_buff *skb, u16 csum_offset)
+{
+ if (likely(skb_partial_csum_set(skb, skb_transport_offset(skb),
+ csum_offset)))
+ return 0;
+ return -EINVAL;
+}
+
+static __wsum ipxlat_pseudohdr6_csum(const struct ipv6hdr *hdr)
+{
+ return ~csum_unfold(csum_ipv6_magic(&hdr->saddr, &hdr->daddr, 0, 0, 0));
+}
+
+static __wsum ipxlat_pseudohdr4_csum(const struct iphdr *hdr)
+{
+ return csum_tcpudp_nofold(hdr->saddr, hdr->daddr, 0, 0, 0);
+}
+
+__sum16 ipxlat_46_update_l4_csum(__sum16 csum16, const struct iphdr *in_ip4,
+ const struct ipv6hdr *out_ip6)
+{
+ __wsum csum;
+
+ csum = ~csum_unfold(csum16);
+
+ /* only address terms change for TCP and UDP */
+ csum = csum_sub(csum, ipxlat_pseudohdr4_csum(in_ip4));
+ csum = csum_add(csum, ipxlat_pseudohdr6_csum(out_ip6));
+ return csum_fold(csum);
+}
+
+__sum16 ipxlat_64_update_l4_csum(__sum16 csum16, const struct ipv6hdr *in_ip6,
+ const struct iphdr *out_ip4)
+{
+ __wsum csum;
+
+ csum = ~csum_unfold(csum16);
+
+ /* only address terms change for TCP and UDP */
+ csum = csum_sub(csum, ipxlat_pseudohdr6_csum(in_ip6));
+ csum = csum_add(csum, ipxlat_pseudohdr4_csum(out_ip4));
+ return csum_fold(csum);
+}
+
+__sum16 ipxlat_l4_csum_ipv6(const struct in6_addr *saddr,
+ const struct in6_addr *daddr,
+ const struct sk_buff *skb, unsigned int l4_off,
+ unsigned int l4_len, u8 proto)
+{
+ return csum_ipv6_magic(saddr, daddr, l4_len, proto,
+ skb_checksum(skb, l4_off, l4_len, 0));
+}
+
+static int ipxlat_ensure_tailroom(struct sk_buff *skb, const unsigned int grow)
+{
+ int err;
+
+ if (!grow || skb_tailroom(skb) >= grow)
+ return 0;
+
+ /* tail growth may reallocate backing storage and move skb data */
+ err = pskb_expand_head(skb, 0, grow - skb_tailroom(skb), GFP_ATOMIC);
+ if (unlikely(err))
+ return err;
+
+ return 0;
+}
+
+int ipxlat_icmp_relayout(struct sk_buff *skb,
+ const struct ipxlat_icmp_layout *layout)
+{
+ const unsigned int output_ext_off = layout->outer_len +
+ layout->output_quote_len +
+ layout->output_pad_len;
+ const unsigned int input_ext_off = layout->outer_len +
+ layout->input_quote_len;
+ const unsigned int new_len = output_ext_off +
+ layout->output_ext_len;
+ const unsigned int old_len = skb->len;
+ unsigned int grow = 0;
+ int err;
+
+ /* reserve tailroom before extending the translated layout */
+ if (unlikely(new_len > old_len)) {
+ grow = new_len - old_len;
+
+ err = ipxlat_ensure_tailroom(skb, grow);
+ if (unlikely(err))
+ return err;
+
+ __skb_put(skb, grow);
+ }
+
+ if (unlikely(layout->output_ext_len))
+ memmove(skb->data + output_ext_off, skb->data + input_ext_off,
+ layout->output_ext_len);
+
+ if (unlikely(layout->output_pad_len))
+ memset(skb->data + layout->outer_len + layout->output_quote_len,
+ 0, layout->output_pad_len);
+
+ if (unlikely(new_len < old_len)) {
+ err = pskb_trim(skb, new_len);
+ if (unlikely(err))
+ return err;
+ }
+
+ return 0;
+}
+
+int ipxlat_finalize_offload(struct sk_buff *skb, u8 l4_proto, bool is_fragment,
+ u32 gso_from, u32 gso_to)
+{
+ struct skb_shared_info *shinfo;
+ bool preserve_l4_hash;
+
+ if (unlikely(skb->ip_summed == CHECKSUM_COMPLETE))
+ skb->ip_summed = CHECKSUM_NONE;
+
+ if (!skb_is_gso(skb))
+ goto out_hash;
+
+ /* align with forwarding paths that reject LRO skbs before xmit */
+ if (unlikely(skb_warn_if_lro(skb)))
+ return -EINVAL;
+
+ shinfo = skb_shinfo(skb);
+ switch (l4_proto) {
+ case IPPROTO_TCP:
+ /* segment payload size is unchanged by address-family
+ * translation so there's no need to touch gso_size
+ */
+ if (shinfo->gso_type & gso_from) {
+ shinfo->gso_type &= ~gso_from;
+ shinfo->gso_type |= gso_to;
+ } else if (unlikely(!(shinfo->gso_type & gso_to))) {
+ return -EINVAL;
+ }
+ break;
+ case IPPROTO_UDP:
+ break;
+ case IPPROTO_ICMP:
+ /* for ICMP there is no GSO transform: clear stale offload
+ * metadata so the stack treats it as a normal frame
+ */
+ skb_gso_reset(skb);
+ break;
+ default:
+ /* other GSO types need an explicit cross-family mapping */
+ return -EINVAL;
+ }
+
+out_hash:
+ preserve_l4_hash = !is_fragment &&
+ (l4_proto == IPPROTO_TCP || l4_proto == IPPROTO_UDP);
+ if (likely(preserve_l4_hash))
+ skb_clear_hash_if_not_l4(skb);
+ else
+ skb_clear_hash(skb);
+ return 0;
+}
diff --git a/net/ipxlat/transport.h b/net/ipxlat/transport.h
new file mode 100644
index 000000000000..a82776abccc8
--- /dev/null
+++ b/net/ipxlat/transport.h
@@ -0,0 +1,127 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#ifndef _NET_IPXLAT_TRANSPORT_H_
+#define _NET_IPXLAT_TRANSPORT_H_
+
+#include <linux/icmp.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+
+/**
+ * ipxlat_l4_min_len - minimum transport header size for protocol
+ * @protocol: transport protocol identifier
+ *
+ * Return: minimum header length for @protocol, or 0 when unsupported.
+ */
+static inline unsigned int ipxlat_l4_min_len(u8 protocol)
+{
+ switch (protocol) {
+ case IPPROTO_TCP:
+ return sizeof(struct tcphdr);
+ case IPPROTO_UDP:
+ return sizeof(struct udphdr);
+ case IPPROTO_ICMP:
+ return sizeof(struct icmphdr);
+ default:
+ return 0;
+ }
+}
+
+/**
+ * ipxlat_set_partial_csum - program CHECKSUM_PARTIAL metadata on skb
+ * @skb: packet with transport checksum field
+ * @csum_offset: offset of checksum field within transport header
+ *
+ * Return: 0 on success, negative errno on invalid skb state.
+ */
+int ipxlat_set_partial_csum(struct sk_buff *skb, u16 csum_offset);
+
+/**
+ * ipxlat_46_update_l4_csum - replace IPv4 pseudo-header checksum terms
+ * @csum16: folded TCP or UDP checksum
+ * @in_ip4: original IPv4 header
+ * @out_ip6: translated IPv6 header
+ *
+ * Return: checksum adjusted for the translated addresses.
+ */
+__sum16 ipxlat_46_update_l4_csum(__sum16 csum16, const struct iphdr *in_ip4,
+ const struct ipv6hdr *out_ip6);
+
+/**
+ * ipxlat_64_update_l4_csum - replace IPv6 pseudo-header checksum terms
+ * @csum16: folded TCP or UDP checksum
+ * @in_ip6: original IPv6 header
+ * @out_ip4: translated IPv4 header
+ *
+ * Return: checksum adjusted for the translated addresses.
+ */
+__sum16 ipxlat_64_update_l4_csum(__sum16 csum16, const struct ipv6hdr *in_ip6,
+ const struct iphdr *out_ip4);
+
+/**
+ * ipxlat_l4_csum_ipv6 - compute full L4 checksum with IPv6 pseudo-header
+ * @saddr: IPv6 source address
+ * @daddr: IPv6 destination address
+ * @skb: packet buffer
+ * @l4_off: transport header offset
+ * @l4_len: transport span (header + payload)
+ * @proto: transport protocol
+ *
+ * Return: folded checksum value covering pseudo-header and transport payload.
+ */
+__sum16 ipxlat_l4_csum_ipv6(const struct in6_addr *saddr,
+ const struct in6_addr *daddr,
+ const struct sk_buff *skb, unsigned int l4_off,
+ unsigned int l4_len, u8 proto);
+
+struct ipxlat_icmp_layout {
+ unsigned int outer_len;
+ unsigned int input_quote_len;
+ unsigned int input_ext_len;
+ unsigned int output_quote_len;
+ unsigned int output_pad_len;
+ unsigned int output_ext_len;
+};
+
+/**
+ * ipxlat_icmp_relayout - resize quoted ICMP payload/extensions in place
+ * @skb: packet buffer
+ * @layout: input and output quote/extension layout
+ *
+ * Moves extension bytes, adds the requested padding and grows or trims the skb
+ * to match @layout.
+ *
+ * Return: 0 on success, negative errno on resize/memory failures.
+ */
+int ipxlat_icmp_relayout(struct sk_buff *skb,
+ const struct ipxlat_icmp_layout *layout);
+
+/**
+ * ipxlat_finalize_offload - normalize checksum/GSO metadata after translation
+ * @skb: translated packet
+ * @l4_proto: IPv4-side transport protocol
+ * @is_fragment: resulting packet is fragmented
+ * @gso_from: input TCP GSO type bit
+ * @gso_to: output TCP GSO type bit
+ *
+ * Remaps TCP GSO family bits, preserves UDP GSO and clears ICMP GSO metadata.
+ * It also rejects LRO or incompatible GSO representations and clears stale
+ * packet hashes when tuple semantics changed.
+ *
+ * Return: 0 on success, negative errno on incompatible offload metadata.
+ */
+int ipxlat_finalize_offload(struct sk_buff *skb, u8 l4_proto, bool is_fragment,
+ u32 gso_from, u32 gso_to);
+
+#endif /* _NET_IPXLAT_TRANSPORT_H_ */
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 11/18] net: ipxlat: add IPv4/IPv6 header and transport translation
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (9 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 10/18] net: ipxlat: add checksum and offload helpers Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 12/18] net: ipxlat: add translation preparation and dispatch Ralf Lici
` (6 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Daniel Gröber, Antonio Quartulli,
linux-kernel
Rewrite IPv4 and IPv6 headers in place, map Fragment Headers and update
TCP and UDP checksums for ordinary, partial-checksum and GSO packets.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-8-ralf@mandelbit.com/
- Drive the L3 and TCP/UDP rewrite paths only from prepared metadata and
configuration, without routing or device policy.
- Base 6-to-4 GSO DF selection on each eventual IPv4 segment and leave
DF clear for legacy UFO.
- Make the 6-to-4 skb writable before moving or changing headers.
(Sashiko)
- Reacquire header pointers after operations which can move packet
storage.
- Use the kernel's zero length-field convention for oversized GSO
packets instead of truncating translated lengths, and reject
unrepresentable non-GSO packets. (Sashiko)
- Preserve CSUM_MANGLED_0 when an outer 4-to-6 UDP checksum update
produces zero. (Sashiko)
net/ipxlat/Makefile | 2 +
net/ipxlat/translate_46.c | 213 ++++++++++++++++++++++++++++++++++++
net/ipxlat/translate_46.h | 77 +++++++++++++
net/ipxlat/translate_64.c | 222 ++++++++++++++++++++++++++++++++++++++
net/ipxlat/translate_64.h | 96 +++++++++++++++++
5 files changed, 610 insertions(+)
create mode 100644 net/ipxlat/translate_46.c
create mode 100644 net/ipxlat/translate_46.h
create mode 100644 net/ipxlat/translate_64.c
create mode 100644 net/ipxlat/translate_64.h
diff --git a/net/ipxlat/Makefile b/net/ipxlat/Makefile
index 91baab41d5eb..b0fab158c7b3 100644
--- a/net/ipxlat/Makefile
+++ b/net/ipxlat/Makefile
@@ -10,3 +10,5 @@ ipxlat-objs += address.o
ipxlat-objs += packet4.o
ipxlat-objs += packet6.o
ipxlat-objs += transport.o
+ipxlat-objs += translate_46.o
+ipxlat-objs += translate_64.o
diff --git a/net/ipxlat/translate_46.c b/net/ipxlat/translate_46.c
new file mode 100644
index 000000000000..add29d9ef0a7
--- /dev/null
+++ b/net/ipxlat/translate_46.c
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <net/ip6_checksum.h>
+#include <net/tcp.h>
+#include <net/udp.h>
+
+#include <net/ipxlat.h>
+
+#include "address.h"
+#include "packet.h"
+#include "transport.h"
+#include "translate_46.h"
+
+u8 ipxlat_46_map_proto_to_nexthdr(u8 protocol)
+{
+ return (protocol == IPPROTO_ICMP) ? NEXTHDR_ICMP : protocol;
+}
+
+static __be16 ipxlat_46_build_frag_off(u16 frag_offset, bool mf)
+{
+ return cpu_to_be16((frag_offset & IP6_OFFSET) | (mf ? IP6_MF : 0));
+}
+
+void ipxlat_46_build_frag_hdr(struct frag_hdr *fh6, const struct iphdr *hdr4,
+ u8 l4_proto)
+{
+ const u16 frag_off4 = ipxlat_get_frag4_offset(hdr4);
+ const bool mf = !!(be16_to_cpu(hdr4->frag_off) & IP_MF);
+
+ fh6->nexthdr = ipxlat_46_map_proto_to_nexthdr(l4_proto);
+ fh6->reserved = 0;
+ fh6->frag_off = ipxlat_46_build_frag_off(frag_off4, mf);
+ fh6->identification = cpu_to_be32(be16_to_cpu(hdr4->id));
+}
+
+void ipxlat_46_build_l3_hdr(struct ipv6hdr *iph6, const struct iphdr *iph4,
+ unsigned int l3_payload_len, u8 nexthdr,
+ u8 hop_limit)
+{
+ iph6->version = 6;
+ iph6->priority = iph4->tos >> 4;
+ iph6->flow_lbl[0] = (iph4->tos & 0x0F) << 4;
+ iph6->flow_lbl[1] = 0;
+ iph6->flow_lbl[2] = 0;
+ ipv6_set_payload_len(iph6, l3_payload_len);
+ iph6->nexthdr = nexthdr;
+ iph6->hop_limit = hop_limit;
+}
+
+int ipxlat_46_prepare_udp_csum(struct sk_buff *skb)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ struct udphdr *udp = udp_hdr(skb);
+ struct iphdr *iph4 = ip_hdr(skb);
+ unsigned int udp_len;
+ __sum16 csum;
+
+ if (!ipxlat_is_first_frag4(iph4) || skb->ip_summed == CHECKSUM_PARTIAL)
+ return 0;
+ if (likely(udp->check))
+ return 0;
+
+ udp_len = ntohs(udp->len);
+ if (unlikely(skb_ensure_writable(skb, cb->l4_off + sizeof(*udp))))
+ return -ENOMEM;
+
+ /* skb_ensure_writable might move pointers so re-acquire the headers */
+ iph4 = ip_hdr(skb);
+ udp = udp_hdr(skb);
+ udp->check = 0;
+ csum = csum_tcpudp_magic(iph4->saddr, iph4->daddr, udp_len, IPPROTO_UDP,
+ skb_checksum(skb, cb->l4_off, udp_len, 0));
+ udp->check = csum ? csum : CSUM_MANGLED_0;
+ return 0;
+}
+
+static int ipxlat_46_translate_tcp_csum(struct sk_buff *skb,
+ const struct iphdr *in4)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct ipv6hdr *out6 = ipv6_hdr(skb);
+ struct tcphdr *tcp = tcp_hdr(skb);
+
+ /* CHECKSUM_PARTIAL contains a pseudo-header seed rather than a final
+ * checksum. Replace it with the translated IPv6 pseudo-header and
+ * leave completion to the later output path.
+ */
+ if (skb->ip_summed == CHECKSUM_PARTIAL) {
+ tcp->check = ~tcp_v6_check(ipxlat_skb_l4_seg_len(skb, cb),
+ &out6->saddr, &out6->daddr, 0);
+ return ipxlat_set_partial_csum(skb,
+ offsetof(struct tcphdr, check));
+ }
+
+ tcp->check = ipxlat_46_update_l4_csum(tcp->check, in4, out6);
+ skb->ip_summed = CHECKSUM_NONE;
+ return 0;
+}
+
+static int ipxlat_46_translate_udp_csum(struct sk_buff *skb,
+ const struct iphdr *in4)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct ipv6hdr *out6 = ipv6_hdr(skb);
+ struct udphdr *udp = udp_hdr(skb);
+
+ if (skb->ip_summed == CHECKSUM_PARTIAL) {
+ udp->check = ~udp_v6_check(ipxlat_skb_l4_seg_len(skb, cb),
+ &out6->saddr, &out6->daddr, 0);
+ return ipxlat_set_partial_csum(skb,
+ offsetof(struct udphdr, check));
+ }
+
+ udp->check = ipxlat_46_update_l4_csum(udp->check, in4, out6);
+ if (unlikely(!udp->check))
+ udp->check = CSUM_MANGLED_0;
+ skb->ip_summed = CHECKSUM_NONE;
+ return 0;
+}
+
+int ipxlat_46_translate(const struct ipxlat_config *cfg, struct sk_buff *skb)
+{
+ unsigned int l3_payload_len, min_l4_len, old_l3_len, new_l3_len;
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct iphdr outer4 = *ip_hdr(skb);
+ bool has_frag, first_frag;
+ u8 l4_proto4, l4_proto6;
+ struct frag_hdr *fh6;
+ struct ipv6hdr *iph6;
+ int l3_delta, err;
+
+ /* snapshot the original IPv4 header fields before skb layout changes */
+ l4_proto4 = cb->l4_proto;
+ l4_proto6 = ipxlat_46_map_proto_to_nexthdr(l4_proto4);
+ has_frag = ip_is_fragment(&outer4);
+ first_frag = ipxlat_is_first_frag4(&outer4);
+
+ old_l3_len = cb->l3_hdr_len;
+ new_l3_len = sizeof(struct ipv6hdr) +
+ (has_frag ? sizeof(struct frag_hdr) : 0);
+ l3_delta = (int)new_l3_len - (int)old_l3_len;
+
+ /* reserve headroom and make the transport header writable */
+ if (unlikely(skb_cow_head(skb, max_t(int, 0, l3_delta))))
+ return -ENOMEM;
+ if (first_frag) {
+ min_l4_len = ipxlat_l4_min_len(l4_proto4);
+ if (unlikely(skb_ensure_writable(skb, cb->l4_off + min_l4_len)))
+ return -ENOMEM;
+ }
+
+ /* replace outer L3 area: drop IPv4 hdr, reserve IPv6(+Frag) hdr */
+ ipxlat_replace_l3(skb, old_l3_len, new_l3_len, new_l3_len);
+ skb->protocol = htons(ETH_P_IPV6);
+
+ /* parsed offsets include the old L3 length, which the delta replaces */
+ ipxlat_cb_rebase_offsets(cb, l3_delta);
+ DEBUG_NET_WARN_ON_ONCE(!ipxlat_cb_offsets_valid(cb));
+
+ cb->l3_hdr_len = new_l3_len;
+ cb->fragh_off = has_frag ? sizeof(struct ipv6hdr) : 0;
+ cb->l4_proto = l4_proto6;
+
+ /* build outer IPv6 base hdr from translated IPv4 fields */
+ iph6 = ipv6_hdr(skb);
+ l3_payload_len = skb->len - skb_network_offset(skb) - sizeof(*iph6);
+ ipxlat_46_build_l3_hdr(iph6, &outer4, l3_payload_len, l4_proto6,
+ outer4.ttl);
+ ipxlat_46_convert_endpoints(cfg, &outer4, &iph6->saddr, &iph6->daddr);
+
+ /* add IPv6 fragment hdr when the IPv4 packet carried fragmentation */
+ if (unlikely(has_frag)) {
+ iph6->nexthdr = NEXTHDR_FRAGMENT;
+
+ fh6 = (struct frag_hdr *)(iph6 + 1);
+ ipxlat_46_build_frag_hdr(fh6, &outer4, l4_proto4);
+ }
+
+ /* non-first fragments have no transport header to translate */
+ if (unlikely(!first_frag))
+ goto out;
+
+ /* translate transport hdr and pseudohdr dependent checksums */
+ switch (l4_proto4) {
+ case IPPROTO_TCP:
+ err = ipxlat_46_translate_tcp_csum(skb, &outer4);
+ break;
+ case IPPROTO_UDP:
+ err = ipxlat_46_translate_udp_csum(skb, &outer4);
+ break;
+ default:
+ err = 0;
+ break;
+ }
+ if (unlikely(err))
+ return err;
+
+out:
+ /* normalize checksum/offload metadata for the translated frame */
+ return ipxlat_finalize_offload(skb, l4_proto4, has_frag, SKB_GSO_TCPV4,
+ SKB_GSO_TCPV6);
+}
diff --git a/net/ipxlat/translate_46.h b/net/ipxlat/translate_46.h
new file mode 100644
index 000000000000..b3a63284e641
--- /dev/null
+++ b/net/ipxlat/translate_46.h
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#ifndef _NET_IPXLAT_TRANSLATE_46_H_
+#define _NET_IPXLAT_TRANSLATE_46_H_
+
+#include <net/ipxlat.h>
+
+struct iphdr;
+struct ipv6hdr;
+struct frag_hdr;
+struct sk_buff;
+
+/**
+ * ipxlat_46_map_proto_to_nexthdr - map IPv4 L4 protocol to IPv6 nexthdr
+ * @protocol: IPv4 L4 protocol
+ *
+ * Return: IPv6 next-header value corresponding to @protocol.
+ */
+u8 ipxlat_46_map_proto_to_nexthdr(u8 protocol);
+
+/**
+ * ipxlat_46_build_frag_hdr - build IPv6 Fragment Header from IPv4 fragment info
+ * @fh6: output IPv6 fragment header
+ * @hdr4: source IPv4 header
+ * @l4_proto: original IPv4 L4 protocol
+ */
+void ipxlat_46_build_frag_hdr(struct frag_hdr *fh6, const struct iphdr *hdr4,
+ u8 l4_proto);
+
+/**
+ * ipxlat_46_build_l3_hdr - build translated IPv6 header from IPv4 metadata
+ * @iph6: output IPv6 header
+ * @iph4: source IPv4 header
+ * @l3_payload_len: IPv6 payload length
+ * @nexthdr: resulting IPv6 nexthdr
+ * @hop_limit: resulting IPv6 hop limit
+ */
+void ipxlat_46_build_l3_hdr(struct ipv6hdr *iph6, const struct iphdr *iph4,
+ unsigned int l3_payload_len, u8 nexthdr,
+ u8 hop_limit);
+
+/**
+ * ipxlat_46_prepare_udp_csum - checksum a validated zero-checksum UDP packet
+ * @skb: original IPv4 packet
+ *
+ * This runs after translation eligibility is known. Packets which already
+ * carry a checksum are unchanged.
+ *
+ * Return: 0 on success or %-ENOMEM when the UDP header cannot be made
+ * writable.
+ */
+int ipxlat_46_prepare_udp_csum(struct sk_buff *skb);
+
+/**
+ * ipxlat_46_translate - translate outer packet from IPv4 to IPv6 in place
+ * @cfg: immutable route translation configuration
+ * @skb: packet to translate
+ *
+ * Rewrites outer L3 in place, rebases cached offsets and translates L4 on
+ * first fragments only.
+ *
+ * Return: 0 on success, negative errno on translation failure.
+ */
+int ipxlat_46_translate(const struct ipxlat_config *cfg, struct sk_buff *skb);
+
+#endif /* _NET_IPXLAT_TRANSLATE_46_H_ */
diff --git a/net/ipxlat/translate_64.c b/net/ipxlat/translate_64.c
new file mode 100644
index 000000000000..2448d3da32e4
--- /dev/null
+++ b/net/ipxlat/translate_64.c
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <linux/icmpv6.h>
+#include <net/ip.h>
+#include <net/tcp.h>
+#include <net/udp.h>
+
+#include "translate_64.h"
+#include "packet.h"
+#include "transport.h"
+
+u8 ipxlat_64_map_nexthdr_proto(u8 nexthdr)
+{
+ return (nexthdr == NEXTHDR_ICMP) ? IPPROTO_ICMP : nexthdr;
+}
+
+void ipxlat_64_build_l3_hdr(struct net *net, struct iphdr *iph4,
+ const struct ipv6hdr *iph6,
+ unsigned int tot_len, __be16 frag_off, u8 protocol,
+ __be32 saddr, __be32 daddr, u8 ttl,
+ const struct frag_hdr *frag6)
+{
+ iph4->version = 4;
+ iph4->ihl = 5;
+ iph4->tos = ipxlat_get_ipv6_tclass(iph6);
+ iph_set_totlen(iph4, tot_len);
+ iph4->frag_off = frag_off;
+ iph4->ttl = ttl;
+ iph4->protocol = protocol;
+ iph4->saddr = saddr;
+ iph4->daddr = daddr;
+ iph4->id = 0;
+ if (frag6)
+ iph4->id = ipxlat_64_map_frag_id(frag6);
+ else
+ __ip_select_ident(net, iph4, 1);
+ iph4->check = 0;
+ iph4->check = ip_fast_csum(iph4, iph4->ihl);
+}
+
+static int ipxlat_64_translate_tcp_csum(struct sk_buff *skb,
+ const struct ipv6hdr *in6)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct iphdr *out4 = ip_hdr(skb);
+ struct tcphdr *tcp = tcp_hdr(skb);
+
+ if (skb->ip_summed == CHECKSUM_PARTIAL) {
+ tcp->check = ~tcp_v4_check(ipxlat_skb_l4_seg_len(skb, cb),
+ out4->saddr, out4->daddr, 0);
+ return ipxlat_set_partial_csum(skb,
+ offsetof(struct tcphdr, check));
+ }
+
+ tcp->check = ipxlat_64_update_l4_csum(tcp->check, in6, out4);
+ skb->ip_summed = CHECKSUM_NONE;
+ return 0;
+}
+
+static int ipxlat_64_translate_udp_csum(struct sk_buff *skb,
+ const struct ipv6hdr *in6)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct iphdr *out4 = ip_hdr(skb);
+ struct udphdr *udp = udp_hdr(skb);
+
+ if (skb->ip_summed == CHECKSUM_PARTIAL) {
+ udp->check = ~udp_v4_check(ipxlat_skb_l4_seg_len(skb, cb),
+ out4->saddr, out4->daddr, 0);
+ return ipxlat_set_partial_csum(skb,
+ offsetof(struct udphdr, check));
+ }
+ /* A zero checksum is invalid on ordinary IPv6 UDP, but specialized
+ * receive paths can allow it. It is a valid checksum choice in IPv4.
+ */
+ if (!udp->check) {
+ skb->ip_summed = CHECKSUM_NONE;
+ return 0;
+ }
+
+ udp->check = ipxlat_64_update_l4_csum(udp->check, in6, out4);
+ if (unlikely(!udp->check))
+ udp->check = CSUM_MANGLED_0;
+ skb->ip_summed = CHECKSUM_NONE;
+ return 0;
+}
+
+static __be16 ipxlat_64_build_frag_off(const struct sk_buff *skb,
+ const struct frag_hdr *frag6)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ unsigned int ipv4_seg_len, l4_seg_len;
+ bool df, is_gso, mf;
+ u16 frag_offset;
+
+ /* an IPv6 Fragment Header maps to IPv4 fragmentation with DF clear */
+ if (frag6) {
+ mf = !!(be16_to_cpu(frag6->frag_off) & IP6_MF);
+ frag_offset = ipxlat_get_frag6_offset(frag6);
+ return ipxlat_64_encode_frag_off(false, mf, frag_offset);
+ }
+
+ /* legacy UFO represents one UDP datagram that will be materialized as
+ * fragments, so its IPv4 header must leave DF clear
+ */
+ is_gso = skb_is_gso(skb);
+ if (is_gso && (skb_shinfo(skb)->gso_type & SKB_GSO_UDP))
+ return ipxlat_64_encode_frag_off(false, false, 0);
+
+ /* IPv6 cannot use ordinary PMTU discovery below 1280 bytes. The
+ * corresponding IPv4 size is 1260 bytes because its base header is 20
+ * bytes smaller. Clear DF at or below that floor so low-MTU IPv4 links
+ * can fragment the packet; above it, set DF so the IPv6 sender can
+ * learn a smaller usable PMTU.
+ */
+ if (is_gso) {
+ /* TCP and UDP-L4 GSO contain independent datagrams. Decide DF
+ * from the IPv4 packet each eventual segment will become, not
+ * from the size of the coalesced skb.
+ */
+ l4_seg_len = ipxlat_skb_l4_seg_len(skb, cb);
+ ipv4_seg_len = sizeof(struct iphdr) + l4_seg_len;
+ df = ipxlat_64_should_set_df(ipv4_seg_len);
+ return ipxlat_64_encode_frag_off(df, false, 0);
+ }
+
+ df = ipxlat_64_should_set_df(skb->len);
+ return ipxlat_64_encode_frag_off(df, false, 0);
+}
+
+int ipxlat_64_translate(const struct ipxlat_config *cfg, struct net *net,
+ struct sk_buff *skb)
+{
+ unsigned int min_l4_len, old_l3_len, new_l3_len, tot_len4;
+ const struct ipv6hdr outer6 = *ipv6_hdr(skb);
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ struct frag_hdr *frag6 = NULL;
+ struct frag_hdr frag_copy;
+ u8 l4_proto4, l4_proto6;
+ struct iphdr *iph4;
+ int l3_delta, err;
+ __be16 frag_off;
+ bool first_frag;
+
+ /* snapshot original outer IPv6 fields before L3 rewrite */
+ l4_proto6 = cb->l4_proto;
+ l4_proto4 = ipxlat_64_map_nexthdr_proto(l4_proto6);
+ if (unlikely(cb->fragh_off)) {
+ frag_copy = *(struct frag_hdr *)(skb->data + cb->fragh_off);
+ frag6 = &frag_copy;
+ }
+ first_frag = ipxlat_is_first_frag6(frag6);
+
+ old_l3_len = cb->l3_hdr_len;
+ new_l3_len = sizeof(struct iphdr);
+ l3_delta = (int)new_l3_len - (int)old_l3_len;
+
+ /* reserve headroom and make the transport header writable */
+ if (unlikely(skb_cow_head(skb, max_t(int, 0, l3_delta))))
+ return -ENOMEM;
+ if (first_frag) {
+ min_l4_len = ipxlat_l4_min_len(l4_proto4);
+ if (unlikely(skb_ensure_writable(skb, cb->l4_off + min_l4_len)))
+ return -ENOMEM;
+ }
+
+ /* replace outer IPv6 header with IPv4 in-place */
+ ipxlat_replace_l3(skb, old_l3_len, new_l3_len, new_l3_len);
+ skb->protocol = htons(ETH_P_IP);
+
+ /* parsed offsets include the old L3 length, which the delta replaces */
+ ipxlat_cb_rebase_offsets(cb, l3_delta);
+ DEBUG_NET_WARN_ON_ONCE(!ipxlat_cb_offsets_valid(cb));
+
+ cb->l3_hdr_len = sizeof(struct iphdr);
+ cb->fragh_off = 0;
+ cb->l4_proto = l4_proto4;
+
+ /* build outer IPv4 base hdr from translated IPv6 fields */
+ iph4 = ip_hdr(skb);
+ frag_off = ipxlat_64_build_frag_off(skb, frag6);
+ tot_len4 = skb->len - skb_network_offset(skb);
+ ipxlat_64_build_l3_hdr(net, iph4, &outer6, tot_len4, frag_off,
+ l4_proto4, cb->saddr4, cb->daddr4,
+ outer6.hop_limit, frag6);
+
+ /* non-first fragments have no transport header to translate */
+ if (unlikely(!first_frag))
+ goto out;
+
+ /* translate transport hdr and pseudohdr dependent checksums */
+ switch (l4_proto4) {
+ case IPPROTO_TCP:
+ err = ipxlat_64_translate_tcp_csum(skb, &outer6);
+ break;
+ case IPPROTO_UDP:
+ err = ipxlat_64_translate_udp_csum(skb, &outer6);
+ break;
+ default:
+ err = 0;
+ break;
+ }
+ if (unlikely(err))
+ return err;
+
+out:
+ /* normalize checksum/offload metadata for the translated frame */
+ return ipxlat_finalize_offload(skb, l4_proto4,
+ ip_is_fragment(ip_hdr(skb)),
+ SKB_GSO_TCPV6, SKB_GSO_TCPV4);
+}
diff --git a/net/ipxlat/translate_64.h b/net/ipxlat/translate_64.h
new file mode 100644
index 000000000000..f0ed37e6ad91
--- /dev/null
+++ b/net/ipxlat/translate_64.h
@@ -0,0 +1,96 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#ifndef _NET_IPXLAT_TRANSLATE_64_H_
+#define _NET_IPXLAT_TRANSLATE_64_H_
+
+#include <linux/ipv6.h>
+
+#include <net/ipxlat.h>
+
+struct sk_buff;
+struct iphdr;
+struct ipv6hdr;
+
+/**
+ * ipxlat_64_should_set_df - choose DF for IPv6 packets without a Fragment
+ * Header
+ * @pkt_len4: size of the resulting IPv4 packet
+ *
+ * Return: true when the IPv4 packet is above the IPv6-derived PMTU floor.
+ */
+static inline bool ipxlat_64_should_set_df(unsigned int pkt_len4)
+{
+ return pkt_len4 > IPV6_MIN_MTU - sizeof(struct ipv6hdr) +
+ sizeof(struct iphdr);
+}
+
+/**
+ * ipxlat_64_map_frag_id - derive an IPv4 ID from an IPv6 Fragment Header
+ * @frag6: source IPv6 Fragment Header
+ *
+ * Return: the low 16 bits of the IPv6 identification in network byte order.
+ */
+static inline __be16 ipxlat_64_map_frag_id(const struct frag_hdr *frag6)
+{
+ return cpu_to_be16(be32_to_cpu(frag6->identification));
+}
+
+static inline __be16 ipxlat_64_encode_frag_off(bool df, bool mf,
+ u16 frag_offset)
+{
+ return cpu_to_be16((df ? IP_DF : 0) | (mf ? IP_MF : 0) |
+ (frag_offset >> 3));
+}
+
+/**
+ * ipxlat_64_build_l3_hdr - build translated IPv4 header from IPv6 metadata
+ * @net: namespace owning IPv4 identification state
+ * @iph4: output IPv4 header
+ * @iph6: source IPv6 header
+ * @tot_len: resulting IPv4 total length
+ * @frag_off: resulting IPv4 fragment offset/flags
+ * @protocol: resulting IPv4 L4 protocol
+ * @saddr: resulting IPv4 source address
+ * @daddr: resulting IPv4 destination address
+ * @ttl: resulting IPv4 TTL
+ * @frag6: source Fragment Header, or NULL to allocate a fresh IPv4 ID
+ */
+void ipxlat_64_build_l3_hdr(struct net *net, struct iphdr *iph4,
+ const struct ipv6hdr *iph6, unsigned int tot_len,
+ __be16 frag_off, u8 protocol, __be32 saddr,
+ __be32 daddr, u8 ttl, const struct frag_hdr *frag6);
+
+/**
+ * ipxlat_64_translate - translate outer packet from IPv6 to IPv4 in place
+ * @cfg: immutable route translation configuration
+ * @net: namespace owning translated IPv4 fragment-ID state
+ * @skb: packet to translate
+ *
+ * Rewrites outer L3 in place, rebases cached offsets and translates L4 on
+ * first fragments only.
+ *
+ * Return: 0 on success, negative errno on translation failure.
+ */
+int ipxlat_64_translate(const struct ipxlat_config *cfg, struct net *net,
+ struct sk_buff *skb);
+
+/**
+ * ipxlat_64_map_nexthdr_proto - map IPv6 nexthdr to IPv4 L4 protocol
+ * @nexthdr: IPv6 next-header value
+ *
+ * Return: IPv4 protocol value corresponding to @nexthdr.
+ */
+u8 ipxlat_64_map_nexthdr_proto(u8 nexthdr);
+
+#endif /* _NET_IPXLAT_TRANSLATE_64_H_ */
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 12/18] net: ipxlat: add translation preparation and dispatch
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (10 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 11/18] net: ipxlat: add IPv4/IPv6 header and transport translation Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 13/18] net: ipxlat: add translator-generated ICMP errors Ralf Lici
` (5 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Daniel Gröber,
Antonio Quartulli, Toke Høiland-Jørgensen,
linux-kernel
Expose a two-stage engine interface which validates and caches packet
metadata before rewriting. Mark translated DF-clear IPv4 packets so
IPv6 output owns any required fragmentation or refragmentation.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-9-ralf@mandelbit.com/
- Replace the v1 ndo_start_xmit dispatch/actions with the exported
ipxlat_prepare and ipxlat_translate engine interface so attachments
other than the initial LWT harness can reuse the translator. (Toke)
- Complete eligibility checks and protocol preflight before mutation so
unsupported packets can be returned untouched. Allocation failures
during the rewrite still leave the packet for the caller to discard.
- Stop decrementing TTL or Hop Limit inside the engine: the
translated-family forwarding path owns the one forwarding decrement.
(Sashiko)
- Remove pre-fragmentation and mark translated DF-clear IPv4
representations for fragmentation by the target IPv6 output route.
include/net/ipxlat.h | 35 +++++++++++++++++
net/ipxlat/core.c | 92 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 127 insertions(+)
diff --git a/include/net/ipxlat.h b/include/net/ipxlat.h
index 6b8efb2669b8..ddc98058ff20 100644
--- a/include/net/ipxlat.h
+++ b/include/net/ipxlat.h
@@ -16,6 +16,7 @@
#include <linux/in6.h>
#include <linux/types.h>
+#include <net/ip.h>
struct ipxlat_prefix4 {
__be32 addr;
@@ -63,4 +64,38 @@ bool ipxlat_prefix6_len_valid(u8 len);
*/
int ipxlat_config_validate(const struct ipxlat_config *cfg);
+/**
+ * ipxlat_prepare - validate a packet and cache translation metadata
+ * @cfg: validated, immutable translation configuration
+ * @skb: packet to prepare
+ *
+ * The caller must own @skb exclusively. This function uses @skb->cb as parser
+ * storage but does not rewrite packet bytes. On success it sets the transport
+ * header offset. %-EOPNOTSUPP means ipxlat does not handle the packet. A caller
+ * that passes such a packet back to the original stack must restore the old
+ * control buffer.
+ *
+ * Return: 0 when @skb is ready for ipxlat_translate, or a negative errno.
+ */
+int ipxlat_prepare(const struct ipxlat_config *cfg, struct sk_buff *skb);
+
+/**
+ * ipxlat_translate - translate one prepared skb without consuming it
+ * @cfg: configuration used by the preceding ipxlat_prepare call
+ * @net: namespace in which translation runs and whose IPv4 ID state is used
+ * @skb: prepared packet to translate
+ *
+ * The caller must provide a non-NULL @net and must not change @cfg, @skb or
+ * its cached metadata between preparation and translation. Successful
+ * Translation of a DF-clear IPv4 packet sets @skb->ignore_df and marks the
+ * resulting IPv6 packet as locally owned for target-family fragmentation. A
+ * harness which scrubs the skb must preserve both properties.
+ *
+ * Return: 0 with @skb in the translated representation, or a negative errno
+ * with the packet representation unspecified, in which case the caller must
+ * discard it.
+ */
+int ipxlat_translate(const struct ipxlat_config *cfg, struct net *net,
+ struct sk_buff *skb);
+
#endif /* _NET_IPXLAT_H_ */
diff --git a/net/ipxlat/core.c b/net/ipxlat/core.c
index 85037446291e..1dc1fc7e6f04 100644
--- a/net/ipxlat/core.c
+++ b/net/ipxlat/core.c
@@ -12,6 +12,98 @@
*/
#include <linux/module.h>
+#include <net/ip.h>
+#include <net/ipv6.h>
+
+#include <net/ipxlat.h>
+#include "address.h"
+#include "packet.h"
+#include "translate_46.h"
+#include "translate_64.h"
+
+int ipxlat_prepare(const struct ipxlat_config *cfg, struct sk_buff *skb)
+{
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ int ret;
+
+ DEBUG_NET_WARN_ON_ONCE(skb_shared(skb));
+ memset(skb->cb, 0, sizeof(struct ipxlat_cb));
+
+ switch (ntohs(skb->protocol)) {
+ case ETH_P_IPV6:
+ ret = ipxlat_v6_validate_skb(skb);
+ if (unlikely(ret))
+ return ret;
+
+ /* packets with non-translatable IPv6 addresses can be handed
+ * back to the stack unchanged since they might be intended for
+ * native traffic such as ND
+ */
+ ret = ipxlat_64_convert_endpoints(cfg, ipv6_hdr(skb),
+ cb->is_icmp_err, &cb->saddr4,
+ &cb->daddr4);
+ if (unlikely(ret))
+ return -EOPNOTSUPP;
+
+ if (unlikely(cb->l4_proto == NEXTHDR_ICMP))
+ return -EOPNOTSUPP;
+ break;
+ case ETH_P_IP:
+ ret = ipxlat_v4_validate_skb(skb);
+ if (unlikely(ret))
+ return ret;
+
+ if (unlikely(cb->l4_proto == IPPROTO_ICMP))
+ return -EOPNOTSUPP;
+ break;
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ skb_set_transport_header(skb, cb->l4_off);
+ return 0;
+}
+EXPORT_SYMBOL_GPL(ipxlat_prepare);
+
+int ipxlat_translate(const struct ipxlat_config *cfg, struct net *net,
+ struct sk_buff *skb)
+{
+ bool frag_source;
+ int ret;
+
+ switch (ntohs(skb->protocol)) {
+ case ETH_P_IPV6:
+ return ipxlat_64_translate(cfg, net, skb);
+ case ETH_P_IP:
+ frag_source = !(ip_hdr(skb)->frag_off & htons(IP_DF));
+
+ if (ipxlat_skb_cb(skb)->l4_proto == IPPROTO_UDP) {
+ /* calculate the csum of a csumless UDP packet */
+ ret = ipxlat_46_prepare_udp_csum(skb);
+ if (unlikely(ret))
+ return ret;
+ }
+
+ ret = ipxlat_46_translate(cfg, skb);
+ if (unlikely(ret))
+ return ret;
+
+ /* Replace parser state with IPv6 fragmentation metadata.
+ * DF-clear IPv4 packets may be fragmented by the target route,
+ * while IP6SKB_FRAG_SOURCE lets reassembly and ip6_fragment
+ * preserve an existing translated Fragment Header.
+ */
+ memset(IP6CB(skb), 0, sizeof(*IP6CB(skb)));
+ skb->ignore_df = frag_source;
+ if (frag_source)
+ IP6CB(skb)->flags |= IP6SKB_FRAG_SOURCE;
+ return 0;
+ default:
+ DEBUG_NET_WARN_ON_ONCE(1);
+ return -EINVAL;
+ }
+}
+EXPORT_SYMBOL_GPL(ipxlat_translate);
MODULE_DESCRIPTION("IPv6<>IPv4 stateless translation engine (SIIT)");
MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 13/18] net: ipxlat: add translator-generated ICMP errors
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (11 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 12/18] net: ipxlat: add translation preparation and dispatch Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 14/18] net: ipxlat: add ICMP informational translation Ralf Lici
` (4 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Daniel Gröber,
Antonio Quartulli, linux-kernel
Let validation record an ICMP error without emitting it immediately,
and provide an input-side helper which sends the pending error while
the original packet headers and ingress device are still available.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-10-ralf@mandelbit.com/
- Replace the netdevice drop/action path with pending-error metadata
recorded during preparation.
- Add an input-side emission helper which runs while the original
headers and real ingress device are still available.
- Leave local-output failures to the originating stack instead of
inventing an ingress device solely to generate an ICMP error.
include/net/ipxlat.h | 12 ++++++++++++
net/ipxlat/core.c | 40 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 52 insertions(+)
diff --git a/include/net/ipxlat.h b/include/net/ipxlat.h
index ddc98058ff20..592429f4cff5 100644
--- a/include/net/ipxlat.h
+++ b/include/net/ipxlat.h
@@ -98,4 +98,16 @@ int ipxlat_prepare(const struct ipxlat_config *cfg, struct sk_buff *skb);
int ipxlat_translate(const struct ipxlat_config *cfg, struct net *net,
struct sk_buff *skb);
+/**
+ * ipxlat_emit_icmp_error - emit a pending error for a received packet
+ * @dev: real ingress device, required for IPv4 ICMP routing
+ * @skb: original-family input packet; IPv6 emission also uses @skb->dev
+ *
+ * Callers must retain the original headers and must not use it for locally
+ * originated errors. A forwarded packet may reach an output LWT callback, but
+ * it still has an ingress device and a remote sender which needs the error.
+ * It does nothing when the engine did not record an ICMP error.
+ */
+void ipxlat_emit_icmp_error(struct net_device *dev, struct sk_buff *skb);
+
#endif /* _NET_IPXLAT_H_ */
diff --git a/net/ipxlat/core.c b/net/ipxlat/core.c
index 1dc1fc7e6f04..dc03ca3c9f89 100644
--- a/net/ipxlat/core.c
+++ b/net/ipxlat/core.c
@@ -11,8 +11,12 @@
* Ralf Lici <ralf@mandelbit.com>
*/
+#include <linux/icmp.h>
+#include <linux/icmpv6.h>
#include <linux/module.h>
+#include <net/icmp.h>
#include <net/ip.h>
+#include <net/route.h>
#include <net/ipv6.h>
#include <net/ipxlat.h>
@@ -105,5 +109,41 @@ int ipxlat_translate(const struct ipxlat_config *cfg, struct net *net,
}
EXPORT_SYMBOL_GPL(ipxlat_translate);
+void ipxlat_emit_icmp_error(struct net_device *dev, struct sk_buff *skb)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct iphdr *iph = ip_hdr(skb);
+ struct inet6_skb_parm param6 = {};
+ struct inet_skb_parm param4 = {};
+
+ if (!cb->emit_icmp_err)
+ return;
+
+ switch (ntohs(skb->protocol)) {
+ case ETH_P_IP:
+ /* __icmp_send requires an input route on the offending packet.
+ * Rebuild it if the original route was dropped.
+ */
+ if (!skb_dst(skb)) {
+ if (ip_route_input_noref(skb, iph->daddr, iph->saddr,
+ ip4h_dscp(iph), dev))
+ return;
+ }
+ __icmp_send(skb, cb->pending_icmp_err.type,
+ cb->pending_icmp_err.code,
+ htonl(cb->pending_icmp_err.info), ¶m4);
+ break;
+ case ETH_P_IPV6:
+ icmp6_send(skb, cb->pending_icmp_err.type,
+ cb->pending_icmp_err.code,
+ cb->pending_icmp_err.info, NULL, ¶m6);
+ break;
+ default:
+ DEBUG_NET_WARN_ON_ONCE(1);
+ break;
+ }
+}
+EXPORT_SYMBOL_GPL(ipxlat_emit_icmp_error);
+
MODULE_DESCRIPTION("IPv6<>IPv4 stateless translation engine (SIIT)");
MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 14/18] net: ipxlat: add ICMP informational translation
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (12 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 13/18] net: ipxlat: add translator-generated ICMP errors Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 15/18] net: ipxlat: add ICMP error and quoted-packet translation Ralf Lici
` (3 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Daniel Gröber, Antonio Quartulli,
linux-kernel
Translate ICMP echo requests and replies in both directions, including
incremental conversion of complete checksums and reseeding of partial
checksums. Decline ICMP errors until quoted-packet translation is
available.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-12-ralf@mandelbit.com/
- Remove the ICMPv6 pseudo-header seed before producing a
CHECKSUM_PARTIAL informational ICMPv4 packet. (Sashiko)
net/ipxlat/Makefile | 2 +
net/ipxlat/core.c | 15 +++--
net/ipxlat/icmp.h | 70 +++++++++++++++++++++++
net/ipxlat/icmp_46.c | 106 +++++++++++++++++++++++++++++++++++
net/ipxlat/icmp_64.c | 114 ++++++++++++++++++++++++++++++++++++++
net/ipxlat/translate_46.c | 4 ++
net/ipxlat/translate_64.c | 4 ++
7 files changed, 311 insertions(+), 4 deletions(-)
create mode 100644 net/ipxlat/icmp.h
create mode 100644 net/ipxlat/icmp_46.c
create mode 100644 net/ipxlat/icmp_64.c
diff --git a/net/ipxlat/Makefile b/net/ipxlat/Makefile
index b0fab158c7b3..2190cad37c7f 100644
--- a/net/ipxlat/Makefile
+++ b/net/ipxlat/Makefile
@@ -12,3 +12,5 @@ ipxlat-objs += packet6.o
ipxlat-objs += transport.o
ipxlat-objs += translate_46.o
ipxlat-objs += translate_64.o
+ipxlat-objs += icmp_46.o
+ipxlat-objs += icmp_64.o
diff --git a/net/ipxlat/core.c b/net/ipxlat/core.c
index dc03ca3c9f89..5a2d7099c9d9 100644
--- a/net/ipxlat/core.c
+++ b/net/ipxlat/core.c
@@ -22,6 +22,7 @@
#include <net/ipxlat.h>
#include "address.h"
#include "packet.h"
+#include "icmp.h"
#include "translate_46.h"
#include "translate_64.h"
@@ -49,16 +50,22 @@ int ipxlat_prepare(const struct ipxlat_config *cfg, struct sk_buff *skb)
if (unlikely(ret))
return -EOPNOTSUPP;
- if (unlikely(cb->l4_proto == NEXTHDR_ICMP))
- return -EOPNOTSUPP;
+ if (unlikely(cb->l4_proto == NEXTHDR_ICMP)) {
+ ret = ipxlat_64_icmp_preflight(cfg, skb);
+ if (ret)
+ return ret;
+ }
break;
case ETH_P_IP:
ret = ipxlat_v4_validate_skb(skb);
if (unlikely(ret))
return ret;
- if (unlikely(cb->l4_proto == IPPROTO_ICMP))
- return -EOPNOTSUPP;
+ if (unlikely(cb->l4_proto == IPPROTO_ICMP)) {
+ ret = ipxlat_46_icmp_preflight(skb);
+ if (ret)
+ return ret;
+ }
break;
default:
return -EOPNOTSUPP;
diff --git a/net/ipxlat/icmp.h b/net/ipxlat/icmp.h
new file mode 100644
index 000000000000..0e887746efe0
--- /dev/null
+++ b/net/ipxlat/icmp.h
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#ifndef _NET_IPXLAT_ICMP_H_
+#define _NET_IPXLAT_ICMP_H_
+
+#include <linux/ipv6.h>
+
+#include <net/ipxlat.h>
+
+/**
+ * ipxlat_46_icmp_preflight - check whether an ICMPv4 packet is translatable
+ * @skb: validated packet carrying ICMPv4
+ *
+ * This check runs before any packet bytes are rewritten.
+ *
+ * Return: 0 when translatable or %-EOPNOTSUPP when the packet should be left
+ * in its original representation.
+ */
+int ipxlat_46_icmp_preflight(struct sk_buff *skb);
+
+/**
+ * ipxlat_46_icmp - translate ICMP payload after outer 4->6 L3 rewrite
+ * @cfg: immutable route translation configuration
+ * @skb: packet carrying ICMPv4 transport payload
+ *
+ * Handles both ICMP info translation and ICMP error quoted-inner rewriting.
+ *
+ * Return: 0 on success, negative errno on translation failure.
+ */
+int ipxlat_46_icmp(const struct ipxlat_config *cfg, struct sk_buff *skb);
+
+/**
+ * ipxlat_64_icmp_preflight - check whether an ICMPv6 packet is translatable
+ * @cfg: immutable route translation configuration
+ * @skb: validated packet carrying ICMPv6
+ *
+ * This check includes quoted endpoints and runs before any packet bytes are
+ * rewritten.
+ *
+ * Return: 0 when translatable, %-EOPNOTSUPP when the packet should be left in
+ * its original representation, or another negative errno for invalid input.
+ */
+int ipxlat_64_icmp_preflight(const struct ipxlat_config *cfg,
+ struct sk_buff *skb);
+
+/**
+ * ipxlat_64_icmp - translate ICMP payload after outer 6->4 L3 rewrite
+ * @net: namespace owning quoted translated IPv4 fragment-ID state
+ * @skb: packet carrying ICMPv6 transport payload
+ * @in6: snapshot of original outer IPv6 header
+ *
+ * Handles both ICMP info translation and ICMP error quoted-inner rewriting.
+ *
+ * Return: 0 on success, negative errno on translation failure.
+ */
+int ipxlat_64_icmp(struct net *net, struct sk_buff *skb,
+ const struct ipv6hdr *in6);
+
+#endif /* _NET_IPXLAT_ICMP_H_ */
diff --git a/net/ipxlat/icmp_46.c b/net/ipxlat/icmp_46.c
new file mode 100644
index 000000000000..bb3952256162
--- /dev/null
+++ b/net/ipxlat/icmp_46.c
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <net/ip6_checksum.h>
+
+#include "icmp.h"
+#include "packet.h"
+#include "transport.h"
+
+static int ipxlat_46_icmp_map_info_type_code(const struct icmphdr *in,
+ struct icmp6hdr *out)
+{
+ switch (in->type) {
+ case ICMP_ECHO:
+ out->icmp6_type = ICMPV6_ECHO_REQUEST;
+ out->icmp6_code = 0;
+ out->icmp6_identifier = in->un.echo.id;
+ out->icmp6_sequence = in->un.echo.sequence;
+ return 0;
+ case ICMP_ECHOREPLY:
+ out->icmp6_type = ICMPV6_ECHO_REPLY;
+ out->icmp6_code = 0;
+ out->icmp6_identifier = in->un.echo.id;
+ out->icmp6_sequence = in->un.echo.sequence;
+ return 0;
+ }
+
+ return -EPROTONOSUPPORT;
+}
+
+int ipxlat_46_icmp_preflight(struct sk_buff *skb)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct icmphdr *icmp4;
+ struct icmp6hdr mapped = {};
+ int err;
+
+ if (cb->is_icmp_err)
+ return -EOPNOTSUPP;
+
+ icmp4 = (const struct icmphdr *)(skb->data + cb->l4_off);
+ err = ipxlat_46_icmp_map_info_type_code(icmp4, &mapped);
+ return err ? -EOPNOTSUPP : 0;
+}
+
+static void ipxlat_46_icmp_info_update_csum(const struct icmphdr *icmp4,
+ struct icmp6hdr *icmp6,
+ const struct ipv6hdr *ip6,
+ unsigned int l4_len)
+{
+ struct icmp6hdr icmp6_zero = *icmp6;
+ struct icmphdr icmp4_zero = *icmp4;
+ __wsum csum;
+
+ icmp4_zero.checksum = 0;
+ icmp6_zero.icmp6_cksum = 0;
+ csum = ~csum_unfold(icmp4->checksum);
+ csum = csum_sub(csum, csum_partial(&icmp4_zero, sizeof(icmp4_zero), 0));
+ csum = csum_add(csum, csum_partial(&icmp6_zero, sizeof(icmp6_zero), 0));
+ icmp6->icmp6_cksum = csum_ipv6_magic(&ip6->saddr, &ip6->daddr, l4_len,
+ IPPROTO_ICMPV6, csum);
+}
+
+static int ipxlat_46_icmp_info_outer(struct sk_buff *skb)
+{
+ const struct icmphdr icmp4 = *icmp_hdr(skb);
+ const struct ipv6hdr *ip6 = ipv6_hdr(skb);
+ struct icmp6hdr *icmp6 = icmp6_hdr(skb);
+ unsigned int l4_len;
+ int err;
+
+ err = ipxlat_46_icmp_map_info_type_code(&icmp4, icmp6);
+ if (unlikely(err))
+ return -EINVAL;
+
+ l4_len = ipxlat_skb_l4_len(skb);
+ if (skb->ip_summed == CHECKSUM_PARTIAL) {
+ icmp6->icmp6_cksum = ~csum_ipv6_magic(&ip6->saddr, &ip6->daddr,
+ l4_len, IPPROTO_ICMPV6,
+ 0);
+ return ipxlat_set_partial_csum(skb, offsetof(struct icmp6hdr,
+ icmp6_cksum));
+ }
+
+ ipxlat_46_icmp_info_update_csum(&icmp4, icmp6, ip6, l4_len);
+ skb->ip_summed = CHECKSUM_NONE;
+ return 0;
+}
+
+int ipxlat_46_icmp(const struct ipxlat_config *cfg, struct sk_buff *skb)
+{
+ if (unlikely(ipxlat_skb_cb(skb)->is_icmp_err))
+ return -EINVAL;
+
+ return ipxlat_46_icmp_info_outer(skb);
+}
diff --git a/net/ipxlat/icmp_64.c b/net/ipxlat/icmp_64.c
new file mode 100644
index 000000000000..ce3131ffc6be
--- /dev/null
+++ b/net/ipxlat/icmp_64.c
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+ *
+ * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Alberto Leiva Popper <ydahhrk@gmail.com>
+ * Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <net/ip6_checksum.h>
+
+#include "icmp.h"
+#include "packet.h"
+#include "transport.h"
+
+static int ipxlat_64_icmp_map_info_type_code(const struct icmp6hdr *in,
+ struct icmphdr *out)
+{
+ switch (in->icmp6_type) {
+ case ICMPV6_ECHO_REQUEST:
+ out->type = ICMP_ECHO;
+ out->code = 0;
+ out->un.echo.id = in->icmp6_identifier;
+ out->un.echo.sequence = in->icmp6_sequence;
+ return 0;
+ case ICMPV6_ECHO_REPLY:
+ out->type = ICMP_ECHOREPLY;
+ out->code = 0;
+ out->un.echo.id = in->icmp6_identifier;
+ out->un.echo.sequence = in->icmp6_sequence;
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static __sum16 ipxlat_64_compute_icmp_info_csum(const struct ipv6hdr *in6,
+ const struct icmp6hdr *in_icmp6,
+ const struct icmphdr *out_icmp4,
+ unsigned int l4_len)
+{
+ struct icmp6hdr icmp6_zero = *in_icmp6;
+ struct icmphdr icmp4_zero = *out_icmp4;
+ __wsum csum, pseudo_csum;
+
+ icmp6_zero.icmp6_cksum = 0;
+ icmp4_zero.checksum = 0;
+
+ csum = ~csum_unfold(in_icmp6->icmp6_cksum);
+ pseudo_csum = ~csum_unfold(csum_ipv6_magic(&in6->saddr, &in6->daddr,
+ l4_len, NEXTHDR_ICMP, 0));
+ csum = csum_sub(csum, pseudo_csum);
+ csum = csum_sub(csum, csum_partial(&icmp6_zero, sizeof(icmp6_zero), 0));
+ csum = csum_add(csum, csum_partial(&icmp4_zero, sizeof(icmp4_zero), 0));
+ return csum_fold(csum);
+}
+
+static int ipxlat_64_icmp_info_outer(struct sk_buff *skb,
+ const struct ipv6hdr *in6)
+{
+ struct icmp6hdr *ic6 = icmp6_hdr(skb);
+ struct icmphdr *ic4 = icmp_hdr(skb);
+ struct icmp6hdr ic6_copy = *ic6;
+ unsigned int l4_len;
+ int err;
+
+ err = ipxlat_64_icmp_map_info_type_code(&ic6_copy, ic4);
+ if (unlikely(err))
+ return err;
+
+ if (skb->ip_summed == CHECKSUM_PARTIAL) {
+ /* ICMPv4 has no pseudo-header so discard the IPv6 seed and let
+ * checksum completion cover the translated ICMPv4 message
+ */
+ ic4->checksum = 0;
+ return ipxlat_set_partial_csum(skb, offsetof(struct icmphdr,
+ checksum));
+ }
+
+ l4_len = ipxlat_skb_l4_len(skb);
+ ic4->checksum = ipxlat_64_compute_icmp_info_csum(in6, &ic6_copy, ic4,
+ l4_len);
+ skb->ip_summed = CHECKSUM_NONE;
+ return 0;
+}
+
+int ipxlat_64_icmp_preflight(const struct ipxlat_config *cfg,
+ struct sk_buff *skb)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct icmp6hdr *icmp6;
+ struct icmphdr mapped = {};
+ int err;
+
+ if (cb->is_icmp_err)
+ return -EOPNOTSUPP;
+
+ icmp6 = (const struct icmp6hdr *)(skb->data + cb->l4_off);
+ err = ipxlat_64_icmp_map_info_type_code(icmp6, &mapped);
+ return err ? -EOPNOTSUPP : 0;
+}
+
+int ipxlat_64_icmp(struct net *net, struct sk_buff *skb,
+ const struct ipv6hdr *in6)
+{
+ if (unlikely(ipxlat_skb_cb(skb)->is_icmp_err))
+ return -EINVAL;
+
+ return ipxlat_64_icmp_info_outer(skb, in6);
+}
diff --git a/net/ipxlat/translate_46.c b/net/ipxlat/translate_46.c
index add29d9ef0a7..824b429dadb5 100644
--- a/net/ipxlat/translate_46.c
+++ b/net/ipxlat/translate_46.c
@@ -18,6 +18,7 @@
#include <net/ipxlat.h>
#include "address.h"
+#include "icmp.h"
#include "packet.h"
#include "transport.h"
#include "translate_46.h"
@@ -199,6 +200,9 @@ int ipxlat_46_translate(const struct ipxlat_config *cfg, struct sk_buff *skb)
case IPPROTO_UDP:
err = ipxlat_46_translate_udp_csum(skb, &outer4);
break;
+ case IPPROTO_ICMP:
+ err = ipxlat_46_icmp(cfg, skb);
+ break;
default:
err = 0;
break;
diff --git a/net/ipxlat/translate_64.c b/net/ipxlat/translate_64.c
index 2448d3da32e4..3888ec0d8061 100644
--- a/net/ipxlat/translate_64.c
+++ b/net/ipxlat/translate_64.c
@@ -17,6 +17,7 @@
#include <net/udp.h>
#include "translate_64.h"
+#include "icmp.h"
#include "packet.h"
#include "transport.h"
@@ -207,6 +208,9 @@ int ipxlat_64_translate(const struct ipxlat_config *cfg, struct net *net,
case IPPROTO_UDP:
err = ipxlat_64_translate_udp_csum(skb, &outer6);
break;
+ case IPPROTO_ICMP:
+ err = ipxlat_64_icmp(net, skb, &outer6);
+ break;
default:
err = 0;
break;
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 15/18] net: ipxlat: add ICMP error and quoted-packet translation
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (13 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 14/18] net: ipxlat: add ICMP informational translation Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 16/18] net: ipxlat: add lightweight tunnel attachment Ralf Lici
` (2 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Daniel Gröber, Antonio Quartulli,
linux-kernel
Map ICMP error types, codes, pointers and MTUs; translate quoted
inner headers and checksums; and rebuild RFC 4884 quote, padding and
extension layouts in both directions.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-13-ralf@mandelbit.com/
- Accept the minimum eight-byte ICMP quote without requiring a complete
quoted TCP header, and update quoted TCP and UDP checksums only when
enough bytes are present. (Sashiko)
- Preserve CSUM_MANGLED_0 if an incremental quoted-inner UDP checksum
update produces zero. (Sashiko)
- Do not retain quoted-header pointers across operations which can
reallocate packet storage.
net/ipxlat/icmp_46.c | 490 ++++++++++++++++++++++++++++++++++++++++-
net/ipxlat/icmp_64.c | 513 ++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 993 insertions(+), 10 deletions(-)
diff --git a/net/ipxlat/icmp_46.c b/net/ipxlat/icmp_46.c
index bb3952256162..793d8151a859 100644
--- a/net/ipxlat/icmp_46.c
+++ b/net/ipxlat/icmp_46.c
@@ -13,10 +13,50 @@
#include <net/ip6_checksum.h>
+#include "address.h"
#include "icmp.h"
#include "packet.h"
+#include "translate_46.h"
#include "transport.h"
+#define IPXLAT_ICMP4_PP_CODE_PTR 0
+#define IPXLAT_ICMP4_PP_CODE_BADLEN 2
+
+/* RFC 7915 Section 4.2, Figure 3 */
+static const u8 ipxlat_46_icmp_param_prob_map[] = {
+ 0, 1, 4, 4, 0xff, 0xff, 0xff, 0xff, 7, 6,
+ 0xff, 0xff, 8, 8, 8, 8, 24, 24, 24, 24,
+};
+
+/* RFC 1191 plateau table used when ICMPv4 FRAG_NEEDED reports MTU=0 */
+static const u16 ipxlat_46_mtu_plateaus[] = {
+ 65535, 32000, 17914, 8166, 4352, 2002, 1492,
+};
+
+static u8 ipxlat_icmp4_get_param_ptr(const struct icmphdr *ic4)
+{
+ return ntohl(ic4->un.gateway) >> 24;
+}
+
+static int ipxlat_46_icmp_map_param_prob(const struct icmphdr *in,
+ struct icmp6hdr *out)
+{
+ u8 ptr;
+
+ if (unlikely(in->code != IPXLAT_ICMP4_PP_CODE_PTR &&
+ in->code != IPXLAT_ICMP4_PP_CODE_BADLEN))
+ return -EPROTONOSUPPORT;
+
+ ptr = ipxlat_icmp4_get_param_ptr(in);
+ if (unlikely(ptr >= ARRAY_SIZE(ipxlat_46_icmp_param_prob_map) ||
+ ipxlat_46_icmp_param_prob_map[ptr] == 0xff))
+ return -EPROTONOSUPPORT;
+
+ out->icmp6_code = ICMPV6_HDR_FIELD;
+ out->icmp6_pointer = cpu_to_be32(ipxlat_46_icmp_param_prob_map[ptr]);
+ return 0;
+}
+
static int ipxlat_46_icmp_map_info_type_code(const struct icmphdr *in,
struct icmp6hdr *out)
{
@@ -38,18 +78,153 @@ static int ipxlat_46_icmp_map_info_type_code(const struct icmphdr *in,
return -EPROTONOSUPPORT;
}
+static __be32 ipxlat_46_compute_icmp_mtu6(unsigned int pkt_mtu,
+ u16 tot_len_field)
+{
+ unsigned int i;
+ u32 mtu6;
+
+ /* RFC 7915 Section 4.2:
+ * If the IPv4 router set the MTU field to zero, then the translator
+ * MUST use the plateau values specified in RFC 1191 to determine a
+ * likely path MTU and include that path MTU in the ICMPv6 packet.
+ */
+ if (unlikely(pkt_mtu == 0)) {
+ for (i = 0; i < ARRAY_SIZE(ipxlat_46_mtu_plateaus); i++) {
+ if (ipxlat_46_mtu_plateaus[i] < tot_len_field) {
+ pkt_mtu = ipxlat_46_mtu_plateaus[i];
+ break;
+ }
+ }
+ }
+
+ /* The engine has no authoritative next-hop routes for the quoted flow.
+ * Preserve the reported path information, adjust for the larger IPv6
+ * header and enforce the IPv6 minimum MTU. The output path handles its
+ * own route PMTU after translation.
+ */
+ mtu6 = pkt_mtu + sizeof(struct ipv6hdr) - sizeof(struct iphdr);
+ if (mtu6 < IPV6_MIN_MTU)
+ mtu6 = IPV6_MIN_MTU;
+
+ return cpu_to_be32(mtu6);
+}
+
+static int ipxlat_46_build_icmp_dest_unreach(const struct icmphdr *in,
+ struct icmp6hdr *out,
+ const struct iphdr *inner4,
+ bool *ie_forbidden)
+{
+ unsigned int inner4_tot_len, in_frag_mtu;
+
+ *ie_forbidden = false;
+ switch (in->code) {
+ case ICMP_NET_UNREACH:
+ case ICMP_HOST_UNREACH:
+ case ICMP_SR_FAILED:
+ case ICMP_NET_UNKNOWN:
+ case ICMP_HOST_UNKNOWN:
+ case ICMP_HOST_ISOLATED:
+ case ICMP_NET_UNR_TOS:
+ case ICMP_HOST_UNR_TOS:
+ out->icmp6_type = ICMPV6_DEST_UNREACH;
+ out->icmp6_code = ICMPV6_NOROUTE;
+ out->icmp6_unused = 0;
+ return 0;
+ case ICMP_PORT_UNREACH:
+ out->icmp6_type = ICMPV6_DEST_UNREACH;
+ out->icmp6_code = ICMPV6_PORT_UNREACH;
+ out->icmp6_unused = 0;
+ return 0;
+ case ICMP_NET_ANO:
+ case ICMP_HOST_ANO:
+ case ICMP_PKT_FILTERED:
+ case ICMP_PREC_CUTOFF:
+ out->icmp6_type = ICMPV6_DEST_UNREACH;
+ out->icmp6_code = ICMPV6_ADM_PROHIBITED;
+ out->icmp6_unused = 0;
+ return 0;
+ case ICMP_PROT_UNREACH:
+ out->icmp6_type = ICMPV6_PARAMPROB;
+ out->icmp6_code = ICMPV6_UNK_NEXTHDR;
+ out->icmp6_pointer =
+ cpu_to_be32(offsetof(struct ipv6hdr, nexthdr));
+ *ie_forbidden = true;
+ return 0;
+ case ICMP_FRAG_NEEDED:
+ out->icmp6_type = ICMPV6_PKT_TOOBIG;
+ out->icmp6_code = 0;
+ in_frag_mtu = be16_to_cpu(in->un.frag.mtu);
+ inner4_tot_len = be16_to_cpu(inner4->tot_len);
+ out->icmp6_mtu =
+ ipxlat_46_compute_icmp_mtu6(in_frag_mtu,
+ inner4_tot_len);
+ *ie_forbidden = true;
+ return 0;
+ default:
+ return -EPROTONOSUPPORT;
+ }
+}
+
+static int ipxlat_46_icmp_map_error_type_code(const struct icmphdr *in,
+ struct icmp6hdr *out,
+ const struct iphdr *inner4,
+ bool *ie_forbidden)
+{
+ int err;
+
+ *ie_forbidden = false;
+
+ switch (in->type) {
+ case ICMP_DEST_UNREACH:
+ return ipxlat_46_build_icmp_dest_unreach(in, out, inner4,
+ ie_forbidden);
+ case ICMP_TIME_EXCEEDED:
+ out->icmp6_type = ICMPV6_TIME_EXCEED;
+ out->icmp6_code = in->code;
+ out->icmp6_unused = 0;
+ return 0;
+ case ICMP_PARAMETERPROB:
+ out->icmp6_type = ICMPV6_PARAMPROB;
+ *ie_forbidden = true;
+ err = ipxlat_46_icmp_map_param_prob(in, out);
+ if (unlikely(err))
+ return err;
+ return 0;
+ }
+
+ return -EPROTONOSUPPORT;
+}
+
int ipxlat_46_icmp_preflight(struct sk_buff *skb)
{
const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
const struct icmphdr *icmp4;
struct icmp6hdr mapped = {};
+ struct icmphdr inner_icmp4;
+ struct iphdr inner4;
+ bool ie_forbidden;
int err;
- if (cb->is_icmp_err)
+ icmp4 = (const struct icmphdr *)(skb->data + cb->l4_off);
+ if (!cb->is_icmp_err) {
+ err = ipxlat_46_icmp_map_info_type_code(icmp4, &mapped);
+ return err ? -EOPNOTSUPP : 0;
+ }
+
+ /* quoted headers are not guaranteed to be naturally aligned */
+ memcpy(&inner4, skb->data + cb->inner_l3_offset, sizeof(inner4));
+ err = ipxlat_46_icmp_map_error_type_code(icmp4, &mapped, &inner4,
+ &ie_forbidden);
+ if (err)
return -EOPNOTSUPP;
- icmp4 = (const struct icmphdr *)(skb->data + cb->l4_off);
- err = ipxlat_46_icmp_map_info_type_code(icmp4, &mapped);
+ if (!ipxlat_is_first_frag4(&inner4) || inner4.protocol != IPPROTO_ICMP)
+ return 0;
+
+ memcpy(&inner_icmp4, skb->data + cb->inner_l4_offset,
+ sizeof(inner_icmp4));
+ err = ipxlat_46_icmp_map_info_type_code(&inner_icmp4, &mapped);
return err ? -EOPNOTSUPP : 0;
}
@@ -97,10 +272,317 @@ static int ipxlat_46_icmp_info_outer(struct sk_buff *skb)
return 0;
}
+struct ipxlat_46_quote {
+ struct iphdr ip4;
+ unsigned int l3_off;
+ unsigned int l4_off;
+ unsigned int l4_len;
+ unsigned int end;
+ bool is_frag;
+};
+
+static int ipxlat_46_icmp_inner_l3(const struct ipxlat_config *cfg,
+ struct sk_buff *skb,
+ struct ipxlat_46_quote *quote,
+ int *inner_delta)
+{
+ unsigned int inner_l3_payload, inner_l3_len, inner_tot_len;
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ unsigned int input_quote_end, input_quote_len;
+ unsigned int old_l4_off, new_l4_off;
+ struct ipv6hdr outer_ip6_copy;
+ struct ipv6hdr *inner_ip6;
+ int adjusted_quote_end;
+ struct frag_hdr *fh6;
+ u8 next_hdr;
+
+ /* inner header alignment is not guaranteed */
+ quote->l3_off = cb->inner_l3_offset;
+ memcpy("e->ip4, skb->data + quote->l3_off, sizeof(quote->ip4));
+ inner_l3_len = quote->ip4.ihl << 2;
+ quote->is_frag = ip_is_fragment("e->ip4);
+ input_quote_len = icmp_hdr(skb)->un.reserved[1] << 2;
+ input_quote_end = input_quote_len ? cb->payload_off + input_quote_len :
+ skb->len;
+
+ /* save outer IPv6 hdr because pull+push destroys that hdr region */
+ outer_ip6_copy = *ipv6_hdr(skb);
+
+ old_l4_off = quote->l3_off + inner_l3_len;
+ new_l4_off = quote->l3_off + sizeof(struct ipv6hdr) +
+ (quote->is_frag ? sizeof(struct frag_hdr) : 0);
+ *inner_delta = (int)new_l4_off - (int)old_l4_off;
+ adjusted_quote_end = (int)input_quote_end + *inner_delta;
+ quote->end = adjusted_quote_end;
+
+ if (unlikely(skb_cow_head(skb, max_t(int, 0, *inner_delta))))
+ return -ENOMEM;
+
+ ipxlat_replace_l3(skb, old_l4_off, new_l4_off,
+ skb_transport_offset(skb));
+
+ *ipv6_hdr(skb) = outer_ip6_copy;
+ ipv6_hdr(skb)->payload_len = htons(skb->len - sizeof(struct ipv6hdr));
+
+ inner_ip6 = (struct ipv6hdr *)(skb->data + quote->l3_off);
+ /* Use quoted IPv4 total-length, not skb->len: skb->len also includes
+ * ICMP extension bytes at the end, which are not part of the quoted
+ * inner IP datagram length.
+ */
+ inner_tot_len = ntohs(quote->ip4.tot_len);
+ quote->l4_len = inner_tot_len - inner_l3_len;
+ quote->l4_off = new_l4_off;
+ quote->end = min(quote->end, quote->l4_off + quote->l4_len);
+ inner_l3_payload = inner_tot_len - inner_l3_len +
+ (quote->is_frag ? sizeof(struct frag_hdr) : 0);
+ if (quote->is_frag)
+ next_hdr = NEXTHDR_FRAGMENT;
+ else
+ next_hdr = ipxlat_46_map_proto_to_nexthdr(quote->ip4.protocol);
+
+ ipxlat_46_build_l3_hdr(inner_ip6, "e->ip4, inner_l3_payload,
+ next_hdr, quote->ip4.ttl);
+
+ ipxlat_46_convert_endpoints(cfg, "e->ip4, &inner_ip6->saddr,
+ &inner_ip6->daddr);
+
+ if (quote->is_frag) {
+ fh6 = (struct frag_hdr *)(inner_ip6 + 1);
+ ipxlat_46_build_frag_hdr(fh6, "e->ip4, quote->ip4.protocol);
+ }
+
+ return 0;
+}
+
+static int ipxlat_46_icmp_inner_l4(struct sk_buff *skb,
+ const struct ipxlat_46_quote *quote)
+{
+ struct icmp6hdr *inner_icmp6;
+ struct icmphdr inner_icmp4;
+ struct ipv6hdr *inner_ip6;
+ unsigned int write_end;
+ struct tcphdr *tcp;
+ struct udphdr *udp;
+ int err;
+
+ if (unlikely(!ipxlat_is_first_frag4("e->ip4)))
+ return 0;
+
+ inner_ip6 = (struct ipv6hdr *)(skb->data + quote->l3_off);
+ switch (quote->ip4.protocol) {
+ case IPPROTO_TCP:
+ /* Only the pseudo-header addresses change. If the quote reaches
+ * the checksum field, the rest of the TCP message is not needed
+ * for an incremental update.
+ */
+ write_end = quote->l4_off + offsetof(struct tcphdr, check) +
+ sizeof(tcp->check);
+ if (write_end > quote->end)
+ return 0;
+ if (unlikely(skb_ensure_writable(skb, write_end)))
+ return -ENOMEM;
+
+ inner_ip6 = (struct ipv6hdr *)(skb->data + quote->l3_off);
+ tcp = (struct tcphdr *)(skb->data + quote->l4_off);
+ tcp->check = ipxlat_46_update_l4_csum(tcp->check, "e->ip4,
+ inner_ip6);
+ return 0;
+ case IPPROTO_UDP:
+ write_end = quote->l4_off + sizeof(*udp);
+ if (unlikely(write_end > quote->end))
+ return -EINVAL;
+ if (unlikely(skb_ensure_writable(skb, write_end)))
+ return -ENOMEM;
+
+ inner_ip6 = (struct ipv6hdr *)(skb->data + quote->l3_off);
+ udp = (struct udphdr *)(skb->data + quote->l4_off);
+ if (!udp->check)
+ return 0;
+ udp->check = ipxlat_46_update_l4_csum(udp->check, "e->ip4,
+ inner_ip6);
+ if (!udp->check)
+ udp->check = CSUM_MANGLED_0;
+ return 0;
+ case IPPROTO_ICMP:
+ write_end = quote->l4_off + sizeof(inner_icmp4);
+ if (unlikely(write_end > quote->end))
+ return -EINVAL;
+ if (unlikely(skb_ensure_writable(skb, write_end)))
+ return -ENOMEM;
+
+ inner_ip6 = (struct ipv6hdr *)(skb->data + quote->l3_off);
+ /* inner header alignment is not guaranteed */
+ memcpy(&inner_icmp4, skb->data + quote->l4_off,
+ sizeof(inner_icmp4));
+ inner_icmp6 = (struct icmp6hdr *)(skb->data + quote->l4_off);
+ err = ipxlat_46_icmp_map_info_type_code(&inner_icmp4,
+ inner_icmp6);
+ if (unlikely(err))
+ return -EINVAL;
+ if (quote->is_frag) {
+ /* a first fragment contains enough bytes to map the
+ * ICMP header, but not enough to rebuild its checksum
+ * over the full datagram
+ */
+ return 0;
+ }
+ ipxlat_46_icmp_info_update_csum(&inner_icmp4, inner_icmp6,
+ inner_ip6, quote->l4_len);
+ return 0;
+ default:
+ return 0;
+ }
+}
+
+/* Adjust ICMP error quoted-datagram/extensions after inner 4->6 translation.
+ * The inner rewrite changes quoted datagram length; this helper recomputes
+ * RFC 4884 delimiter/padding, preserves extensions only when allowed, and
+ * enforces IPv6 minimum-MTU packet size constraints.
+ */
+static int ipxlat_46_icmp_squeeze_ext(struct sk_buff *skb,
+ unsigned int icmp4_ipl, int inner_delta,
+ bool ie_forbidden)
+{
+ unsigned int max_ext_len, output_quote_units = 0;
+ unsigned int packet_len_cap, payload_len;
+ struct ipxlat_icmp_layout layout = {};
+ int input_quote_len, err;
+ struct icmp6hdr *ic6;
+ struct ipv6hdr *iph6;
+
+ /* icmp4_ipl marks where quoted dgram ends and extension area starts */
+ if (likely(!icmp4_ipl))
+ goto no_extensions;
+
+ layout.outer_len = skb_transport_offset(skb) + sizeof(struct icmp6hdr);
+ payload_len = skb->len - layout.outer_len;
+ input_quote_len = icmp4_ipl + inner_delta;
+ if (unlikely(input_quote_len < 0 || input_quote_len > payload_len))
+ return -EINVAL;
+ layout.input_quote_len = input_quote_len;
+
+ if (likely(layout.input_quote_len == payload_len))
+ goto no_extensions;
+
+ layout.input_ext_len = payload_len - layout.input_quote_len;
+ max_ext_len = IPV6_MIN_MTU -
+ (layout.outer_len + ICMP_EXT_ORIG_DGRAM_MIN_LEN);
+
+ if (unlikely(ie_forbidden || layout.input_ext_len > max_ext_len)) {
+ packet_len_cap = min_t(unsigned int,
+ skb->len - layout.input_ext_len,
+ IPV6_MIN_MTU);
+ layout.output_quote_len = packet_len_cap - layout.outer_len;
+ } else {
+ packet_len_cap = min_t(unsigned int, skb->len, IPV6_MIN_MTU);
+ layout.output_quote_len =
+ round_down(packet_len_cap - layout.input_ext_len -
+ layout.outer_len,
+ sizeof(u64));
+ layout.output_pad_len =
+ max_t(unsigned int, ICMP_EXT_ORIG_DGRAM_MIN_LEN,
+ layout.output_quote_len) -
+ layout.output_quote_len;
+ layout.output_ext_len = layout.input_ext_len;
+ output_quote_units = (layout.output_quote_len +
+ layout.output_pad_len) >> 3;
+ }
+
+ /* if no ext bytes are copied and no pad is written, relayout only
+ * trims/updates lengths and does not require full data writability
+ */
+ if (unlikely(layout.output_ext_len || layout.output_pad_len)) {
+ err = skb_ensure_writable(skb, skb->len);
+ if (unlikely(err))
+ return err;
+ }
+
+ err = ipxlat_icmp_relayout(skb, &layout);
+ if (unlikely(err))
+ return err;
+
+ iph6 = ipv6_hdr(skb);
+ iph6->payload_len = htons(skb->len - sizeof(*iph6));
+
+no_extensions:
+ if (unlikely(skb->len > IPV6_MIN_MTU)) {
+ err = pskb_trim(skb, IPV6_MIN_MTU);
+ if (unlikely(err))
+ return err;
+
+ iph6 = ipv6_hdr(skb);
+ iph6->payload_len = htons(skb->len - sizeof(*iph6));
+ }
+
+ ic6 = icmp6_hdr(skb);
+ ic6->icmp6_datagram_len = output_quote_units;
+ return 0;
+}
+
+/**
+ * ipxlat_46_icmp_error - translate ICMPv4 error payload to ICMPv6 error form
+ * @cfg: immutable route translation configuration
+ * @skb: packet carrying outer ICMPv4 error
+ *
+ * Rewrites the quoted inner datagram in place, maps type/code/fields and
+ * adjusts RFC 4884 datagram/extension layout before recomputing outer checksum.
+ *
+ * Return: 0 on success, negative errno on translation failure.
+ */
+static int ipxlat_46_icmp_error(const struct ipxlat_config *cfg,
+ struct sk_buff *skb)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct icmphdr icmp4 = *icmp_hdr(skb);
+ struct ipxlat_46_quote quote;
+ unsigned int l4_len, l4_off;
+ struct icmp6hdr *icmp6;
+ struct ipv6hdr *iph6;
+ int inner_delta, err;
+ bool ie_forbidden;
+
+ if (unlikely(!cb->is_icmp_err)) {
+ DEBUG_NET_WARN_ON_ONCE(1);
+ return -EINVAL;
+ }
+
+ /* rewrite the quoted IP hdr, then repair transport data when present */
+ err = ipxlat_46_icmp_inner_l3(cfg, skb, "e, &inner_delta);
+ if (unlikely(err))
+ return err;
+ err = ipxlat_46_icmp_inner_l4(skb, "e);
+ if (unlikely(err))
+ return err;
+
+ err = ipxlat_46_icmp_map_error_type_code(&icmp4, icmp6_hdr(skb),
+ "e.ip4, &ie_forbidden);
+ if (unlikely(err))
+ return err;
+
+ err = ipxlat_46_icmp_squeeze_ext(skb, icmp4.un.reserved[1] << 2,
+ inner_delta, ie_forbidden);
+ if (unlikely(err))
+ return err;
+
+ /* error path rewrites quoted packet bytes/lengths, so use full
+ * checksum recomputation instead of incremental update
+ */
+ iph6 = ipv6_hdr(skb);
+ icmp6 = icmp6_hdr(skb);
+ l4_off = skb_transport_offset(skb);
+ l4_len = ipxlat_skb_l4_len(skb);
+ icmp6->icmp6_cksum = 0;
+ icmp6->icmp6_cksum = ipxlat_l4_csum_ipv6(&iph6->saddr, &iph6->daddr,
+ skb, l4_off, l4_len,
+ IPPROTO_ICMPV6);
+ skb->ip_summed = CHECKSUM_NONE;
+ return 0;
+}
+
int ipxlat_46_icmp(const struct ipxlat_config *cfg, struct sk_buff *skb)
{
if (unlikely(ipxlat_skb_cb(skb)->is_icmp_err))
- return -EINVAL;
+ return ipxlat_46_icmp_error(cfg, skb);
return ipxlat_46_icmp_info_outer(skb);
}
diff --git a/net/ipxlat/icmp_64.c b/net/ipxlat/icmp_64.c
index ce3131ffc6be..db3a2c784e61 100644
--- a/net/ipxlat/icmp_64.c
+++ b/net/ipxlat/icmp_64.c
@@ -12,11 +12,39 @@
*/
#include <net/ip6_checksum.h>
+#include <net/route.h>
+#include "address.h"
#include "icmp.h"
#include "packet.h"
+#include "translate_64.h"
#include "transport.h"
+#define IPXLAT_ICMP4_ERROR_MAX_LEN 576U
+
+/* RFC 7915 Section 5.2, Figure 4 */
+static const u8 ipxlat_64_icmp_param_prob_map[] = {
+ 0, 1, 0xff, 0xff, 2, 2, 9, 8, 12, 12,
+ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
+ 12, 12, 12, 12, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+};
+
+static int ipxlat_64_icmp_map_param_prob(u32 ptr6, u32 *ptr4)
+{
+ if (unlikely(ptr6 >= ARRAY_SIZE(ipxlat_64_icmp_param_prob_map) ||
+ ipxlat_64_icmp_param_prob_map[ptr6] == 0xff))
+ return -EPROTONOSUPPORT;
+
+ *ptr4 = ipxlat_64_icmp_param_prob_map[ptr6];
+ return 0;
+}
+
+static void ipxlat_icmp4_set_param_ptr(struct icmphdr *ic4, u8 ptr)
+{
+ ic4->un.gateway = htonl((u32)ptr << 24);
+}
+
static int ipxlat_64_icmp_map_info_type_code(const struct icmp6hdr *in,
struct icmphdr *out)
{
@@ -38,6 +66,81 @@ static int ipxlat_64_icmp_map_info_type_code(const struct icmp6hdr *in,
}
}
+static int ipxlat_64_icmp_map_error_type_code(const struct icmp6hdr *ic6,
+ struct icmphdr *ic4,
+ bool *ie_forbidden)
+{
+ const u32 header_delta = sizeof(struct ipv6hdr) - sizeof(struct iphdr);
+ u32 mtu4, mtu6, ptr6, ptr4;
+ int err;
+
+ *ie_forbidden = false;
+
+ switch (ic6->icmp6_type) {
+ case ICMPV6_DEST_UNREACH:
+ ic4->type = ICMP_DEST_UNREACH;
+ switch (ic6->icmp6_code) {
+ case ICMPV6_NOROUTE:
+ case ICMPV6_NOT_NEIGHBOUR:
+ case ICMPV6_ADDR_UNREACH:
+ ic4->code = ICMP_HOST_UNREACH;
+ break;
+ case ICMPV6_ADM_PROHIBITED:
+ ic4->code = ICMP_HOST_ANO;
+ break;
+ case ICMPV6_PORT_UNREACH:
+ ic4->code = ICMP_PORT_UNREACH;
+ break;
+ default:
+ return -EINVAL;
+ }
+ ic4->un.gateway = 0;
+ return 0;
+ case ICMPV6_TIME_EXCEED:
+ ic4->type = ICMP_TIME_EXCEEDED;
+ ic4->code = ic6->icmp6_code;
+ ic4->un.gateway = 0;
+ return 0;
+ case ICMPV6_PKT_TOOBIG:
+ ic4->type = ICMP_DEST_UNREACH;
+ ic4->code = ICMP_FRAG_NEEDED;
+ ic4->un.frag.__unused = 0;
+ /* The engine has no authoritative next-hop routes for the
+ * quoted flow so we preserve the received path information and
+ * adjust it for the smaller IPv4 header. The output path
+ * handles its own route PMTU.
+ */
+ mtu6 = be32_to_cpu(ic6->icmp6_mtu);
+ mtu4 = mtu6 > header_delta ? mtu6 - header_delta : 0;
+ ic4->un.frag.mtu = cpu_to_be16(min_t(u32, mtu4, IP_MAX_MTU));
+ *ie_forbidden = true;
+ return 0;
+ case ICMPV6_PARAMPROB:
+ ptr6 = be32_to_cpu(ic6->icmp6_dataun.un_data32[0]);
+ switch (ic6->icmp6_code) {
+ case ICMPV6_HDR_FIELD:
+ ic4->type = ICMP_PARAMETERPROB;
+ ic4->code = 0;
+ err = ipxlat_64_icmp_map_param_prob(ptr6, &ptr4);
+ if (unlikely(err))
+ return err;
+ ipxlat_icmp4_set_param_ptr(ic4, ptr4);
+ break;
+ case ICMPV6_UNK_NEXTHDR:
+ ic4->type = ICMP_DEST_UNREACH;
+ ic4->code = ICMP_PROT_UNREACH;
+ ic4->un.gateway = 0;
+ break;
+ default:
+ return -EINVAL;
+ }
+ *ie_forbidden = true;
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
static __sum16 ipxlat_64_compute_icmp_info_csum(const struct ipv6hdr *in6,
const struct icmp6hdr *in_icmp6,
const struct icmphdr *out_icmp4,
@@ -91,24 +194,422 @@ static int ipxlat_64_icmp_info_outer(struct sk_buff *skb,
int ipxlat_64_icmp_preflight(const struct ipxlat_config *cfg,
struct sk_buff *skb)
{
- const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
- const struct icmp6hdr *icmp6;
+ unsigned int inner_datagram_len, inner_l3_len, inner_payload_len;
+ struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct icmp6hdr *icmp6 =
+ (const struct icmp6hdr *)(skb->data + cb->l4_off);
+ struct frag_hdr inner_fragh;
+ struct icmp6hdr inner_icmp6;
struct icmphdr mapped = {};
+ struct ipv6hdr inner6;
+ bool ie_forbidden;
int err;
- if (cb->is_icmp_err)
+ if (!cb->is_icmp_err) {
+ err = ipxlat_64_icmp_map_info_type_code(icmp6, &mapped);
+ return err ? -EOPNOTSUPP : 0;
+ }
+
+ err = ipxlat_64_icmp_map_error_type_code(icmp6, &mapped, &ie_forbidden);
+ if (err)
+ return -EOPNOTSUPP;
+
+ /* quoted headers are not guaranteed to be naturally aligned */
+ memcpy(&inner6, skb->data + cb->inner_l3_offset, sizeof(inner6));
+ inner_l3_len = cb->inner_l4_offset - cb->inner_l3_offset;
+ inner_payload_len = ntohs(inner6.payload_len);
+ inner_datagram_len = sizeof(inner6) + inner_payload_len;
+ if (unlikely(inner_l3_len > inner_datagram_len))
+ return -EINVAL;
+
+ if (inner_datagram_len - inner_l3_len + sizeof(struct iphdr) >
+ IP_MAX_MTU)
+ return -EOPNOTSUPP;
+
+ err = ipxlat_64_convert_endpoints(cfg, &inner6, false,
+ &cb->inner_saddr4, &cb->inner_daddr4);
+ if (err)
return -EOPNOTSUPP;
- icmp6 = (const struct icmp6hdr *)(skb->data + cb->l4_off);
- err = ipxlat_64_icmp_map_info_type_code(icmp6, &mapped);
+ if (cb->inner_l4_proto != NEXTHDR_ICMP)
+ return 0;
+
+ if (cb->inner_fragh_off) {
+ memcpy(&inner_fragh, skb->data + cb->inner_fragh_off,
+ sizeof(inner_fragh));
+ if (!ipxlat_is_first_frag6(&inner_fragh))
+ return 0;
+ }
+
+ memcpy(&inner_icmp6, skb->data + cb->inner_l4_offset,
+ sizeof(inner_icmp6));
+ err = ipxlat_64_icmp_map_info_type_code(&inner_icmp6, &mapped);
return err ? -EOPNOTSUPP : 0;
}
+struct ipxlat_64_quote {
+ struct ipv6hdr ip6;
+ struct frag_hdr frag6;
+ unsigned int l3_off;
+ unsigned int l4_off;
+ unsigned int l4_len;
+ unsigned int end;
+ u16 frag_offset;
+ bool has_frag_hdr;
+ bool more_fragments;
+ bool has_l4;
+ bool l4_len_known;
+};
+
+static void ipxlat_64_icmp_inner_frag(const struct sk_buff *skb,
+ const struct ipxlat_cb *cb,
+ struct ipxlat_64_quote *quote)
+{
+ quote->has_frag_hdr = !!cb->inner_fragh_off;
+ quote->more_fragments = false;
+ quote->frag_offset = 0;
+ quote->has_l4 = true;
+ quote->l4_len_known = true;
+ if (!quote->has_frag_hdr)
+ return;
+
+ memcpy("e->frag6, skb->data + cb->inner_fragh_off,
+ sizeof(quote->frag6));
+ quote->frag_offset = ipxlat_get_frag6_offset("e->frag6);
+ quote->more_fragments = !!(be16_to_cpu(quote->frag6.frag_off) & IP6_MF);
+ /* An atomic fragment has offset zero and M clear, so its L4 header and
+ * complete length are available. A first real fragment exposes L4 but
+ * not the complete length; later fragments expose neither.
+ */
+ quote->has_l4 = !quote->frag_offset;
+ quote->l4_len_known = quote->has_l4 && !quote->more_fragments;
+}
+
+static int ipxlat_64_icmp_inner_l3(struct net *net, struct sk_buff *skb,
+ struct ipxlat_64_quote *quote,
+ int *inner_delta)
+{
+ unsigned int inner_datagram_len, inner_l3_len, inner_tot_len;
+ unsigned int inner_payload_len, input_quote_end, input_quote_len;
+ unsigned int new_l4_off, old_l4_off;
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct iphdr outer4_copy = *ip_hdr(skb);
+ struct iphdr *inner4;
+ u8 inner_l4_proto;
+ __be16 frag_off4;
+ bool df;
+ int adjusted_quote_end;
+
+ quote->l3_off = cb->inner_l3_offset;
+ old_l4_off = cb->inner_l4_offset;
+ inner_l3_len = old_l4_off - quote->l3_off;
+
+ inner_l4_proto = ipxlat_64_map_nexthdr_proto(cb->inner_l4_proto);
+
+ /* inner header alignment is not guaranteed */
+ memcpy("e->ip6, skb->data + quote->l3_off, sizeof(quote->ip6));
+ input_quote_len = icmp6_hdr(skb)->icmp6_datagram_len << 3;
+ input_quote_end = input_quote_len ? cb->payload_off + input_quote_len :
+ skb->len;
+
+ /* the quoted IPv6 length is the full datagram length, not the number
+ * of bytes present in this ICMP quote
+ */
+ inner_payload_len = ntohs(quote->ip6.payload_len);
+ inner_datagram_len = sizeof(quote->ip6) + inner_payload_len;
+ if (unlikely(inner_l3_len > inner_datagram_len))
+ return -EINVAL;
+ inner_tot_len = inner_datagram_len - inner_l3_len +
+ sizeof(struct iphdr);
+ if (unlikely(inner_tot_len > IP_MAX_MTU))
+ return -EMSGSIZE;
+ quote->l4_len = inner_tot_len - sizeof(struct iphdr);
+ ipxlat_64_icmp_inner_frag(skb, cb, quote);
+
+ new_l4_off = quote->l3_off + sizeof(struct iphdr);
+ *inner_delta = (int)new_l4_off - (int)old_l4_off;
+ adjusted_quote_end = (int)input_quote_end + *inner_delta;
+ quote->end = adjusted_quote_end;
+ quote->end = min(quote->end, quote->l3_off + inner_tot_len);
+ quote->l4_off = new_l4_off;
+
+ /* unlike 46, inner 6->4 always shrinks quoted L3 size */
+ ipxlat_replace_l3(skb, old_l4_off, new_l4_off,
+ skb_transport_offset(skb));
+
+ *ip_hdr(skb) = outer4_copy;
+
+ inner4 = (struct iphdr *)(skb->data + quote->l3_off);
+ /* RFC 7915 Section 5.1 */
+ if (likely(!quote->has_frag_hdr)) {
+ df = ipxlat_64_should_set_df(inner_tot_len);
+ frag_off4 = ipxlat_64_encode_frag_off(df, false, 0);
+ } else {
+ frag_off4 = ipxlat_64_encode_frag_off(false,
+ quote->more_fragments,
+ quote->frag_offset);
+ }
+
+ ipxlat_64_build_l3_hdr(net, inner4, "e->ip6, inner_tot_len,
+ frag_off4, inner_l4_proto, cb->inner_saddr4,
+ cb->inner_daddr4, quote->ip6.hop_limit,
+ quote->has_frag_hdr ? "e->frag6 : NULL);
+
+ return 0;
+}
+
+static int ipxlat_64_icmp_inner_l4(struct sk_buff *skb,
+ const struct ipxlat_64_quote *quote)
+{
+ struct icmp6hdr inner_icmp6;
+ struct icmphdr *inner_icmp4;
+ unsigned int write_end;
+ struct iphdr *inner4;
+ struct tcphdr *tcp;
+ struct udphdr *udp;
+ int err;
+
+ if (unlikely(!quote->has_l4))
+ return 0;
+
+ inner4 = (struct iphdr *)(skb->data + quote->l3_off);
+ switch (inner4->protocol) {
+ case IPPROTO_TCP:
+ /* Only the pseudo-header addresses change. If the quote reaches
+ * the checksum field, the rest of the TCP message is not needed
+ * for an incremental update.
+ */
+ write_end = quote->l4_off + offsetof(struct tcphdr, check) +
+ sizeof(tcp->check);
+ if (write_end > quote->end)
+ return 0;
+ if (unlikely(skb_ensure_writable(skb, write_end)))
+ return -ENOMEM;
+
+ inner4 = (struct iphdr *)(skb->data + quote->l3_off);
+ tcp = (struct tcphdr *)(skb->data + quote->l4_off);
+ tcp->check = ipxlat_64_update_l4_csum(tcp->check, "e->ip6,
+ inner4);
+ return 0;
+ case IPPROTO_UDP:
+ write_end = quote->l4_off + sizeof(*udp);
+ if (unlikely(write_end > quote->end))
+ return -EINVAL;
+ if (unlikely(skb_ensure_writable(skb, write_end)))
+ return -ENOMEM;
+
+ inner4 = (struct iphdr *)(skb->data + quote->l3_off);
+ udp = (struct udphdr *)(skb->data + quote->l4_off);
+ if (!udp->check)
+ return 0;
+ udp->check = ipxlat_64_update_l4_csum(udp->check, "e->ip6,
+ inner4);
+ if (!udp->check)
+ udp->check = CSUM_MANGLED_0;
+ return 0;
+ case IPPROTO_ICMP:
+ write_end = quote->l4_off + sizeof(inner_icmp6);
+ if (unlikely(write_end > quote->end))
+ return -EINVAL;
+ if (unlikely(skb_ensure_writable(skb, write_end)))
+ return -ENOMEM;
+
+ inner4 = (struct iphdr *)(skb->data + quote->l3_off);
+ /* inner header alignment is not guaranteed */
+ memcpy(&inner_icmp6, skb->data + quote->l4_off,
+ sizeof(inner_icmp6));
+ inner_icmp4 = (struct icmphdr *)(skb->data + quote->l4_off);
+ err = ipxlat_64_icmp_map_info_type_code(&inner_icmp6,
+ inner_icmp4);
+ if (unlikely(err))
+ return err;
+ if (!quote->l4_len_known) {
+ /* a first fragment contains enough bytes to map the
+ * ICMP header, but not enough to rebuild its checksum
+ * over the full datagram
+ */
+ return 0;
+ }
+ inner_icmp4->checksum =
+ ipxlat_64_compute_icmp_info_csum("e->ip6,
+ &inner_icmp6,
+ inner_icmp4,
+ quote->l4_len);
+ return 0;
+ default:
+ return 0;
+ }
+}
+
+/* Rebuild ICMPv4 quoted-datagram/extensions after inner 6->4 translation.
+ *
+ * The inner rewrite changes the quoted datagram length. This helper updates
+ * the RFC 4884 delimiter/padding and extension bytes, then enforces the IPv4
+ * ICMP error size cap.
+ *
+ * This is intentionally not a mirror of ipxlat_46_icmp_squeeze_ext:
+ * - 4->6 always writes icmp6_datagram_len (either computed or 0).
+ * - 6->4 updates ICMPv4 datagram-length only when extensions are allowed.
+ * Some mapped ICMPv6 errors set ie_forbidden, and in that case we keep the
+ * ICMPv4 header semantics for that type/code and only relayout/trim payload.
+ */
+static int ipxlat_64_icmp_squeeze_ext(struct sk_buff *skb,
+ unsigned int icmp6_ipl, int inner_delta,
+ bool ie_forbidden)
+{
+ struct ipxlat_icmp_layout layout = {};
+ unsigned int max_ext_len, output_quote_units = 0;
+ unsigned int packet_len_cap, payload_len;
+ unsigned int new_tot_len;
+ int input_quote_len, err;
+ struct icmphdr *ic4;
+ struct iphdr *iph4;
+
+ if (likely(!icmp6_ipl))
+ goto finalize;
+
+ layout.outer_len = skb_transport_offset(skb) + sizeof(struct icmphdr);
+ if (unlikely(skb->len < layout.outer_len))
+ return -EINVAL;
+
+ payload_len = skb->len - layout.outer_len;
+ input_quote_len = (int)icmp6_ipl + inner_delta;
+ if (unlikely(input_quote_len < 0))
+ return -EINVAL;
+ layout.input_quote_len = input_quote_len;
+ if (unlikely(layout.input_quote_len > payload_len))
+ return -EINVAL;
+
+ if (likely(layout.input_quote_len == payload_len))
+ goto finalize;
+
+ layout.input_ext_len = payload_len - layout.input_quote_len;
+ max_ext_len = IPXLAT_ICMP4_ERROR_MAX_LEN -
+ (layout.outer_len + ICMP_EXT_ORIG_DGRAM_MIN_LEN);
+
+ if (unlikely(ie_forbidden)) {
+ layout.output_quote_len = layout.input_quote_len;
+ } else if (unlikely(layout.input_ext_len > max_ext_len)) {
+ packet_len_cap = min_t(unsigned int,
+ skb->len - layout.input_ext_len,
+ IPXLAT_ICMP4_ERROR_MAX_LEN);
+ layout.output_quote_len = packet_len_cap - layout.outer_len;
+ } else {
+ packet_len_cap = min_t(unsigned int, skb->len,
+ IPXLAT_ICMP4_ERROR_MAX_LEN);
+ layout.output_quote_len =
+ round_down(packet_len_cap - layout.input_ext_len -
+ layout.outer_len,
+ sizeof(u32));
+ layout.output_pad_len =
+ max_t(unsigned int, ICMP_EXT_ORIG_DGRAM_MIN_LEN,
+ layout.output_quote_len) -
+ layout.output_quote_len;
+ layout.output_ext_len = layout.input_ext_len;
+ /* RFC 4884 field is in 32-bit units for ICMPv4 errors */
+ output_quote_units = (layout.output_quote_len +
+ layout.output_pad_len) >> 2;
+ }
+
+ /* if no extension bytes are copied and no pad is written, relayout
+ * only trims/updates lengths and doesn't require full data writability
+ */
+ if (unlikely(layout.output_ext_len || layout.output_pad_len)) {
+ err = skb_ensure_writable(skb, skb->len);
+ if (unlikely(err))
+ return err;
+ }
+
+ err = ipxlat_icmp_relayout(skb, &layout);
+ if (unlikely(err))
+ return err;
+
+finalize:
+ if (!ie_forbidden) {
+ ic4 = icmp_hdr(skb);
+ ic4->un.reserved[1] = output_quote_units;
+ }
+
+ if (unlikely(skb->len > IPXLAT_ICMP4_ERROR_MAX_LEN)) {
+ err = pskb_trim(skb, IPXLAT_ICMP4_ERROR_MAX_LEN);
+ if (unlikely(err))
+ return err;
+ }
+
+ iph4 = ip_hdr(skb);
+ new_tot_len = skb->len;
+ if (unlikely(be16_to_cpu(iph4->tot_len) != new_tot_len)) {
+ iph4->tot_len = cpu_to_be16(new_tot_len);
+ /* relayout/trim may invalidate precomputed DF decision */
+ iph4->frag_off &= cpu_to_be16(~IP_DF);
+ iph4->check = 0;
+ iph4->check = ip_fast_csum(iph4, iph4->ihl);
+ }
+
+ return 0;
+}
+
+/**
+ * ipxlat_64_icmp_error - translate ICMPv6 error payload to ICMPv4 error form
+ * @net: namespace owning translated IPv4 fragment-ID state
+ * @skb: packet carrying outer ICMPv6 error
+ *
+ * Rewrites the quoted inner datagram in place, maps type/code/fields and
+ * adjusts RFC 4884 datagram/extension layout before recomputing outer checksum.
+ *
+ * Return: 0 on success, negative errno on translation failure.
+ */
+static int ipxlat_64_icmp_error(struct net *net, struct sk_buff *skb)
+{
+ const struct ipxlat_cb *cb = ipxlat_skb_cb(skb);
+ const struct icmp6hdr ic6 = *icmp6_hdr(skb);
+ unsigned int icmp6_ipl, l4_len, l4_off;
+ struct ipxlat_64_quote quote;
+ int inner_delta, err;
+ struct icmphdr *ic4;
+ bool ie_forbidden;
+
+ if (unlikely(!cb->is_icmp_err)) {
+ DEBUG_NET_WARN_ON_ONCE(1);
+ return -EINVAL;
+ }
+
+ /* rewrite the quoted IP hdr, then repair transport data when present */
+ err = ipxlat_64_icmp_inner_l3(net, skb, "e, &inner_delta);
+ if (unlikely(err))
+ return err;
+ err = ipxlat_64_icmp_inner_l4(skb, "e);
+ if (unlikely(err))
+ return err;
+
+ /* build outer ICMPv4 error header after inner relayout */
+ ic4 = icmp_hdr(skb);
+ err = ipxlat_64_icmp_map_error_type_code(&ic6, ic4, &ie_forbidden);
+ if (unlikely(err))
+ return err;
+
+ icmp6_ipl = ic6.icmp6_datagram_len << 3;
+ err = ipxlat_64_icmp_squeeze_ext(skb, icmp6_ipl, inner_delta,
+ ie_forbidden);
+ if (unlikely(err))
+ return err;
+
+ /* relayout may reallocate the skb, so reload the header pointer */
+ ic4 = icmp_hdr(skb);
+ /* recompute whole ICMPv4 checksum after error-path relayout */
+ l4_off = skb_transport_offset(skb);
+ l4_len = ipxlat_skb_l4_len(skb);
+ ic4->checksum = 0;
+ ic4->checksum = csum_fold(skb_checksum(skb, l4_off, l4_len, 0));
+ skb->ip_summed = CHECKSUM_NONE;
+ return 0;
+}
+
int ipxlat_64_icmp(struct net *net, struct sk_buff *skb,
const struct ipv6hdr *in6)
{
if (unlikely(ipxlat_skb_cb(skb)->is_icmp_err))
- return -EINVAL;
+ return ipxlat_64_icmp_error(net, skb);
return ipxlat_64_icmp_info_outer(skb, in6);
}
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 16/18] net: ipxlat: add lightweight tunnel attachment
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (14 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 15/18] net: ipxlat: add ICMP error and quoted-packet translation Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 17/18] selftests: net: add ipxlat coverage Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 18/18] docs: networking: add ipxlat guide Ralf Lici
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Daniel Gröber,
Antonio Quartulli, Toke Høiland-Jørgensen,
Beniamino Galvani, linux-kernel
Attach the reusable translator to IPv4 and IPv6 routes. Parse immutable
route configuration, support input and output LWT callbacks, return
ineligible packets to the original route, and re-enter the translated
family through routing and netfilter.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-14-ralf@mandelbit.com/
- New attachment replacing the v1 virtual netdevice. (Toke)
- Parse an immutable RFC 6052 prefix and optional one-element EAMT from
route LWT attributes (normal route, multipath, and nexthop-object
lifetimes own the state).
- Support both input and output LWT callbacks. Return packets rejected
as unsupported during preparation to the route's native callback.
- Scrub original-family metadata, perform a fresh translated-family
route and XFRM lookup, and continue received traffic through
PRE_ROUTING or local traffic through LOCAL_OUT. Populate only
fragment-stable flow selectors.
- Let translated-family forwarding own TTL/Hop Limit and PMTU behavior,
and expose translated packets to ordinary conntrack and NAT.
- Register the LWT module alias used by route-encapsulation autoloading
rather than retaining the v1 rtnl-link alias. (Sashiko)
include/uapi/linux/ipxlat.h | 56 +++
include/uapi/linux/lwtunnel.h | 1 +
net/core/lwtunnel.c | 3 +-
net/ipxlat/Kconfig | 11 +
net/ipxlat/Makefile | 5 +-
net/ipxlat/lwt.c | 643 ++++++++++++++++++++++++++++++++++
6 files changed, 717 insertions(+), 2 deletions(-)
create mode 100644 include/uapi/linux/ipxlat.h
create mode 100644 net/ipxlat/lwt.c
diff --git a/include/uapi/linux/ipxlat.h b/include/uapi/linux/ipxlat.h
new file mode 100644
index 000000000000..018de958f699
--- /dev/null
+++ b/include/uapi/linux/ipxlat.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) LWT configuration
+ *
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+/*
+ * IPXLAT lightweight-tunnel configuration.
+ *
+ * The attributes below are carried in RTA_ENCAP for
+ * LWTUNNEL_ENCAP_IPXLAT. Prefix addresses are raw network-byte-order
+ * address values; prefix lengths are expressed in bits.
+ */
+#ifndef _UAPI_LINUX_IPXLAT_H
+#define _UAPI_LINUX_IPXLAT_H
+
+#include <linux/types.h>
+
+enum {
+ IPXLAT_A_UNSPEC,
+ IPXLAT_A_RFC6052_PREFIX6,
+ /* nested array: each indexed element contains IPXLAT_A_EAM_*
+ * attributes
+ */
+ IPXLAT_A_EAMT,
+ __IPXLAT_A_MAX,
+};
+
+#define IPXLAT_A_MAX (__IPXLAT_A_MAX - 1)
+
+enum {
+ IPXLAT_A_PREFIX6_UNSPEC,
+ IPXLAT_A_PREFIX6_ADDR,
+ IPXLAT_A_PREFIX6_LEN,
+ __IPXLAT_A_PREFIX6_MAX,
+};
+
+#define IPXLAT_A_PREFIX6_MAX (__IPXLAT_A_PREFIX6_MAX - 1)
+
+enum {
+ IPXLAT_A_EAM_UNSPEC,
+ IPXLAT_A_EAM_PREFIX4,
+ IPXLAT_A_EAM_PREFIX4_LEN,
+ IPXLAT_A_EAM_PREFIX6,
+ IPXLAT_A_EAM_PREFIX6_LEN,
+ __IPXLAT_A_EAM_MAX,
+};
+
+#define IPXLAT_A_EAM_MAX (__IPXLAT_A_EAM_MAX - 1)
+
+#endif /* _UAPI_LINUX_IPXLAT_H */
diff --git a/include/uapi/linux/lwtunnel.h b/include/uapi/linux/lwtunnel.h
index 229655ef792f..34538de6baf7 100644
--- a/include/uapi/linux/lwtunnel.h
+++ b/include/uapi/linux/lwtunnel.h
@@ -16,6 +16,7 @@ enum lwtunnel_encap_types {
LWTUNNEL_ENCAP_RPL,
LWTUNNEL_ENCAP_IOAM6,
LWTUNNEL_ENCAP_XFRM,
+ LWTUNNEL_ENCAP_IPXLAT,
__LWTUNNEL_ENCAP_MAX,
};
diff --git a/net/core/lwtunnel.c b/net/core/lwtunnel.c
index db21b9ff6f86..c3028124126f 100644
--- a/net/core/lwtunnel.c
+++ b/net/core/lwtunnel.c
@@ -53,6 +53,8 @@ static const char *lwtunnel_encap_str(enum lwtunnel_encap_types encap_type)
case LWTUNNEL_ENCAP_XFRM:
/* module autoload not supported for encap type */
return NULL;
+ case LWTUNNEL_ENCAP_IPXLAT:
+ return "IPXLAT";
case LWTUNNEL_ENCAP_IP6:
case LWTUNNEL_ENCAP_IP:
case LWTUNNEL_ENCAP_NONE:
@@ -200,7 +202,6 @@ int lwtunnel_valid_encap_type_attr(struct nlattr *attr, int remaining,
return -EINVAL;
}
encap_type = nla_get_u16(nla_entype);
-
if (lwtunnel_valid_encap_type(encap_type, extack))
return -EOPNOTSUPP;
}
diff --git a/net/ipxlat/Kconfig b/net/ipxlat/Kconfig
index 6b0e47f8ce90..b73a443eef0f 100644
--- a/net/ipxlat/Kconfig
+++ b/net/ipxlat/Kconfig
@@ -12,3 +12,14 @@ config NET_IPXLAT
SIIT-DC, 464XLAT network architectures.
To compile this engine as a module, choose M here.
+
+config NET_IPXLAT_LWT
+ tristate "IPv6<>IPv4 packet translation LWT attachment"
+ depends on NET_IPXLAT
+ depends on LWTUNNEL
+ help
+ Attach the ipxlat translation engine to IPv4 or IPv6 routes using the
+ lightweight-tunnel route encapsulation. Routes select the packets to
+ translate and contain their address mapping. Translated packets then
+ enter normal routing and netfilter processing for their new address
+ family.
diff --git a/net/ipxlat/Makefile b/net/ipxlat/Makefile
index 2190cad37c7f..6dc58be53e4d 100644
--- a/net/ipxlat/Makefile
+++ b/net/ipxlat/Makefile
@@ -1,8 +1,9 @@
# SPDX-License-Identifier: GPL-2.0
#
-# IPXLAT - Stateless IP/ICMP Translation (SIIT) engine
+# IPXLAT - Stateless IP/ICMP Translation (SIIT) engine and LWT attachment
obj-$(CONFIG_NET_IPXLAT) += ipxlat.o
+obj-$(CONFIG_NET_IPXLAT_LWT) += ipxlat_lwt.o
ipxlat-objs += config.o
ipxlat-objs += core.o
@@ -14,3 +15,5 @@ ipxlat-objs += translate_46.o
ipxlat-objs += translate_64.o
ipxlat-objs += icmp_46.o
ipxlat-objs += icmp_64.o
+
+ipxlat_lwt-objs += lwt.o
diff --git a/net/ipxlat/lwt.c b/net/ipxlat/lwt.c
new file mode 100644
index 000000000000..6b26edb5f6b0
--- /dev/null
+++ b/net/ipxlat/lwt.c
@@ -0,0 +1,643 @@
+// SPDX-License-Identifier: GPL-2.0
+/* IPXLAT - Stateless IP/ICMP Translation (SIIT) LWT harness
+ *
+ * Copyright (C) 2026- Mandelbit SRL
+ * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+ *
+ * Author: Antonio Quartulli <antonio@mandelbit.com>
+ * Daniel Gröber <dxld@darkboxed.org>
+ * Ralf Lici <ralf@mandelbit.com>
+ */
+
+#include <linux/errno.h>
+#include <linux/icmp.h>
+#include <linux/icmpv6.h>
+#include <linux/inetdevice.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/lwtunnel.h>
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <linux/skbuff.h>
+#include <net/icmp.h>
+#include <net/flow_dissector.h>
+#include <net/ip.h>
+#include <net/ip6_route.h>
+#include <net/ipv6.h>
+#include <net/lwtunnel.h>
+#include <net/route.h>
+#include <net/sock.h>
+#include <uapi/linux/ipxlat.h>
+
+#include <net/ipxlat.h>
+
+struct ipxlat_lwt {
+ struct ipxlat_config config;
+};
+
+static struct ipxlat_lwt *ipxlat_lwt_state(const struct lwtunnel_state *lws)
+{
+ return (struct ipxlat_lwt *)lws->data;
+}
+
+static const struct nla_policy ipxlat_prefix6_policy[IPXLAT_A_PREFIX6_MAX + 1] = {
+ [IPXLAT_A_PREFIX6_ADDR] = NLA_POLICY_EXACT_LEN(sizeof(struct in6_addr)),
+ [IPXLAT_A_PREFIX6_LEN] = { .type = NLA_U8 },
+};
+
+static const struct nla_policy ipxlat_eam_policy[IPXLAT_A_EAM_MAX + 1] = {
+ [IPXLAT_A_EAM_PREFIX4] = NLA_POLICY_EXACT_LEN(sizeof(__be32)),
+ [IPXLAT_A_EAM_PREFIX4_LEN] = { .type = NLA_U8 },
+ [IPXLAT_A_EAM_PREFIX6] =
+ NLA_POLICY_EXACT_LEN(sizeof(struct in6_addr)),
+ [IPXLAT_A_EAM_PREFIX6_LEN] = { .type = NLA_U8 },
+};
+
+static const struct nla_policy ipxlat_lwt_policy[IPXLAT_A_MAX + 1] = {
+ [IPXLAT_A_RFC6052_PREFIX6] =
+ NLA_POLICY_NESTED(ipxlat_prefix6_policy),
+ [IPXLAT_A_EAMT] =
+ NLA_POLICY_NESTED_ARRAY(ipxlat_eam_policy),
+};
+
+static void ipxlat_normalize_prefix6(struct ipxlat_prefix6 *prefix)
+{
+ struct in6_addr original = prefix->addr;
+
+ ipv6_addr_prefix(&prefix->addr, &original, prefix->len);
+}
+
+static void ipxlat_normalize_prefix4(__be32 *prefix, u8 len)
+{
+ *prefix &= inet_make_mask(len);
+}
+
+static int ipxlat_parse_prefix6(struct nlattr *nla,
+ struct ipxlat_prefix6 *prefix,
+ struct netlink_ext_ack *extack)
+{
+ struct nlattr *tb[IPXLAT_A_PREFIX6_MAX + 1];
+ int err;
+
+ err = nla_parse_nested_deprecated(tb, IPXLAT_A_PREFIX6_MAX, nla,
+ ipxlat_prefix6_policy, extack);
+ if (err)
+ return err;
+
+ if (!tb[IPXLAT_A_PREFIX6_ADDR] || !tb[IPXLAT_A_PREFIX6_LEN]) {
+ NL_SET_ERR_MSG_ATTR(extack, nla,
+ "ipxlat rfc6052-prefix6 needs address and prefix length");
+ return -EINVAL;
+ }
+
+ prefix->addr = nla_get_in6_addr(tb[IPXLAT_A_PREFIX6_ADDR]);
+ prefix->len = nla_get_u8(tb[IPXLAT_A_PREFIX6_LEN]);
+ if (!ipxlat_prefix6_len_valid(prefix->len)) {
+ NL_SET_ERR_MSG_ATTR(extack, tb[IPXLAT_A_PREFIX6_LEN],
+ "unsupported RFC 6052 prefix length");
+ return -EINVAL;
+ }
+
+ ipxlat_normalize_prefix6(prefix);
+ return 0;
+}
+
+static int ipxlat_parse_eam(struct nlattr *nla, struct ipxlat_eam *eam,
+ struct netlink_ext_ack *extack)
+{
+ struct nlattr *tb[IPXLAT_A_EAM_MAX + 1];
+ struct in6_addr original, prefix6;
+ __be32 prefix4;
+ int err;
+
+ err = nla_parse_nested_deprecated(tb, IPXLAT_A_EAM_MAX, nla,
+ ipxlat_eam_policy, extack);
+ if (err)
+ return err;
+
+ if (!tb[IPXLAT_A_EAM_PREFIX4] ||
+ !tb[IPXLAT_A_EAM_PREFIX4_LEN] ||
+ !tb[IPXLAT_A_EAM_PREFIX6] ||
+ !tb[IPXLAT_A_EAM_PREFIX6_LEN]) {
+ NL_SET_ERR_MSG_ATTR(extack, nla,
+ "ipxlat EAM needs both IPv4 and IPv6 prefixes");
+ return -EINVAL;
+ }
+
+ eam->prefix4.addr = nla_get_be32(tb[IPXLAT_A_EAM_PREFIX4]);
+ eam->prefix4.len = nla_get_u8(tb[IPXLAT_A_EAM_PREFIX4_LEN]);
+ eam->prefix6.addr = nla_get_in6_addr(tb[IPXLAT_A_EAM_PREFIX6]);
+ eam->prefix6.len = nla_get_u8(tb[IPXLAT_A_EAM_PREFIX6_LEN]);
+ if (eam->prefix4.len > 32 || eam->prefix6.len > 128) {
+ NL_SET_ERR_MSG_ATTR(extack, nla,
+ "EAM prefix length is out of range");
+ return -EINVAL;
+ }
+ if (32 - eam->prefix4.len > 128 - eam->prefix6.len) {
+ NL_SET_ERR_MSG_ATTR(extack, nla,
+ "EAM IPv6 prefix has insufficient host space");
+ return -EINVAL;
+ }
+
+ prefix4 = eam->prefix4.addr;
+ prefix6 = eam->prefix6.addr;
+ original = prefix6;
+ ipxlat_normalize_prefix4(&prefix4, eam->prefix4.len);
+ ipv6_addr_prefix(&prefix6, &original, eam->prefix6.len);
+ if (prefix4 != eam->prefix4.addr ||
+ !ipv6_addr_equal(&prefix6, &eam->prefix6.addr)) {
+ NL_SET_ERR_MSG_ATTR(extack, nla,
+ "EAM prefixes must be canonical");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ipxlat_parse_eamt(struct nlattr *attrs, struct nlattr *eamt,
+ struct ipxlat_config *config,
+ struct netlink_ext_ack *extack)
+{
+ int rem, count, err;
+ struct nlattr *pos;
+
+ count = 0;
+ nla_for_each_nested(pos, attrs, rem) {
+ if (nla_type(pos) != IPXLAT_A_EAMT)
+ continue;
+ if (++count > 1) {
+ NL_SET_ERR_MSG(extack, "duplicate EAMT attribute");
+ return -EINVAL;
+ }
+ }
+
+ if (!eamt)
+ return 0;
+
+ count = 0;
+ nla_for_each_nested(pos, eamt, rem) {
+ if (++count > 1) {
+ NL_SET_ERR_MSG(extack,
+ "ipxlat supports at most one EAM");
+ return -EINVAL;
+ }
+ err = ipxlat_parse_eam(pos, &config->eam, extack);
+ if (err)
+ return err;
+ }
+ config->has_eam = count != 0;
+ return 0;
+}
+
+static int ipxlat_build_state(struct net *net, struct nlattr *nla,
+ unsigned int family, const void *cfg,
+ struct lwtunnel_state **ts,
+ struct netlink_ext_ack *extack)
+{
+ struct nlattr *tb[IPXLAT_A_MAX + 1];
+ struct ipxlat_config config = {};
+ struct lwtunnel_state *newts;
+ struct ipxlat_lwt *ilwt;
+ int err;
+
+ if (family != AF_INET && family != AF_INET6) {
+ NL_SET_ERR_MSG(extack,
+ "ipxlat LWT requires an IPv4 or IPv6 route");
+ return -EAFNOSUPPORT;
+ }
+
+ err = nla_parse_nested_deprecated(tb, IPXLAT_A_MAX, nla,
+ ipxlat_lwt_policy, extack);
+ if (err)
+ return err;
+
+ if (!tb[IPXLAT_A_RFC6052_PREFIX6]) {
+ NL_SET_ERR_MSG(extack, "ipxlat rfc6052-prefix6 is required");
+ return -EINVAL;
+ }
+ err = ipxlat_parse_prefix6(tb[IPXLAT_A_RFC6052_PREFIX6],
+ &config.rfc6052_prefix6, extack);
+ if (err)
+ return err;
+
+ err = ipxlat_parse_eamt(nla, tb[IPXLAT_A_EAMT], &config, extack);
+ if (err)
+ return err;
+
+ err = ipxlat_config_validate(&config);
+ if (err) {
+ NL_SET_ERR_MSG(extack,
+ "invalid normalized ipxlat prefix config");
+ return err;
+ }
+
+ newts = lwtunnel_state_alloc(sizeof(*ilwt));
+ if (!newts)
+ return -ENOMEM;
+
+ ilwt = ipxlat_lwt_state(newts);
+ ilwt->config = config;
+ newts->type = LWTUNNEL_ENCAP_IPXLAT;
+ newts->flags = LWTUNNEL_STATE_INPUT_REDIRECT |
+ LWTUNNEL_STATE_OUTPUT_REDIRECT;
+ *ts = newts;
+
+ return 0;
+}
+
+static int ipxlat_fill_encap(struct sk_buff *skb, struct lwtunnel_state *lws)
+{
+ const struct ipxlat_config *cfg = &ipxlat_lwt_state(lws)->config;
+ struct nlattr *prefix, *eamt, *eam;
+
+ prefix = nla_nest_start_noflag(skb, IPXLAT_A_RFC6052_PREFIX6);
+ if (!prefix)
+ return -EMSGSIZE;
+
+ if (nla_put_in6_addr(skb, IPXLAT_A_PREFIX6_ADDR,
+ &cfg->rfc6052_prefix6.addr) ||
+ nla_put_u8(skb, IPXLAT_A_PREFIX6_LEN, cfg->rfc6052_prefix6.len))
+ goto cancel_prefix;
+ nla_nest_end(skb, prefix);
+
+ if (!cfg->has_eam)
+ return 0;
+
+ eamt = nla_nest_start_noflag(skb, IPXLAT_A_EAMT);
+ if (!eamt)
+ return -EMSGSIZE;
+
+ eam = nla_nest_start_noflag(skb, 1);
+ if (!eam)
+ goto cancel_eamt;
+
+ if (nla_put(skb, IPXLAT_A_EAM_PREFIX4, sizeof(__be32),
+ &cfg->eam.prefix4.addr) ||
+ nla_put_u8(skb, IPXLAT_A_EAM_PREFIX4_LEN,
+ cfg->eam.prefix4.len) ||
+ nla_put_in6_addr(skb, IPXLAT_A_EAM_PREFIX6,
+ &cfg->eam.prefix6.addr) ||
+ nla_put_u8(skb, IPXLAT_A_EAM_PREFIX6_LEN,
+ cfg->eam.prefix6.len))
+ goto cancel_eam;
+ nla_nest_end(skb, eam);
+ nla_nest_end(skb, eamt);
+ return 0;
+
+cancel_eam:
+ nla_nest_cancel(skb, eam);
+
+cancel_eamt:
+ nla_nest_cancel(skb, eamt);
+ return -EMSGSIZE;
+cancel_prefix:
+ nla_nest_cancel(skb, prefix);
+ return -EMSGSIZE;
+}
+
+static int ipxlat_get_encap_size(struct lwtunnel_state *lws)
+{
+ const struct ipxlat_config *cfg = &ipxlat_lwt_state(lws)->config;
+ int eam_payload_size, eam_size, prefix_payload_size, size;
+
+ prefix_payload_size = nla_total_size(sizeof(struct in6_addr)) +
+ nla_total_size(sizeof(u8));
+ size = nla_total_size(prefix_payload_size);
+ if (cfg->has_eam) {
+ eam_payload_size = nla_total_size(sizeof(__be32)) +
+ nla_total_size(sizeof(u8)) +
+ nla_total_size(sizeof(struct in6_addr)) +
+ nla_total_size(sizeof(u8));
+ eam_size = nla_total_size(eam_payload_size);
+ size += nla_total_size(eam_size);
+ }
+
+ return size;
+}
+
+static int ipxlat_cmp_encap(struct lwtunnel_state *a, struct lwtunnel_state *b)
+{
+ const struct ipxlat_config *ac = &ipxlat_lwt_state(a)->config;
+ const struct ipxlat_config *bc = &ipxlat_lwt_state(b)->config;
+
+ if (ac->has_eam != bc->has_eam ||
+ ac->rfc6052_prefix6.len != bc->rfc6052_prefix6.len ||
+ !ipv6_addr_equal(&ac->rfc6052_prefix6.addr,
+ &bc->rfc6052_prefix6.addr))
+ return 1;
+ if (!ac->has_eam)
+ return 0;
+ return ac->eam.prefix4.addr != bc->eam.prefix4.addr ||
+ ac->eam.prefix4.len != bc->eam.prefix4.len ||
+ ac->eam.prefix6.len != bc->eam.prefix6.len ||
+ !ipv6_addr_equal(&ac->eam.prefix6.addr,
+ &bc->eam.prefix6.addr);
+}
+
+/* Populate routing and XFRM selectors from the target-family packet.
+ * Fragmented packets deliberately use only addresses and protocol so every
+ * fragment follows the same policy.
+ */
+static void ipxlat_flowi4_selectors(const struct sk_buff *skb,
+ struct flowi4 *fl4)
+{
+ struct flow_dissector_key_icmp icmp = {};
+ struct flow_keys keys;
+
+ /* classify the translated packet, not an encapsulated inner flow */
+ if (!skb_flow_dissect_flow_keys(skb, &keys,
+ FLOW_DISSECTOR_F_STOP_AT_ENCAP))
+ return;
+
+ fl4->flowi4_proto = keys.basic.ip_proto;
+ if (keys.control.flags & FLOW_DIS_IS_FRAGMENT)
+ return;
+
+ fl4->fl4_sport = keys.ports.src;
+ fl4->fl4_dport = keys.ports.dst;
+ if (keys.basic.ip_proto == IPPROTO_GRE) {
+ fl4->fl4_gre_key = keys.keyid.keyid;
+ } else if (keys.basic.ip_proto == IPPROTO_ICMP) {
+ /* the default flow-keys dissector does not collect ICMP data */
+ skb_flow_get_icmp_tci(skb, &icmp, skb->data,
+ skb_transport_offset(skb),
+ skb_headlen(skb));
+ fl4->fl4_icmp_type = icmp.type;
+ fl4->fl4_icmp_code = icmp.code;
+ }
+}
+
+static void ipxlat_flowi6_selectors(const struct sk_buff *skb,
+ struct flowi6 *fl6)
+{
+ struct flow_dissector_key_icmp icmp = {};
+ struct flow_keys keys;
+
+ /* classify the translated packet, not an encapsulated inner flow */
+ if (!skb_flow_dissect_flow_keys(skb, &keys,
+ FLOW_DISSECTOR_F_STOP_AT_ENCAP))
+ return;
+
+ fl6->flowi6_proto = keys.basic.ip_proto;
+ if (keys.control.flags & FLOW_DIS_IS_FRAGMENT)
+ return;
+
+ fl6->fl6_sport = keys.ports.src;
+ fl6->fl6_dport = keys.ports.dst;
+ if (keys.basic.ip_proto == IPPROTO_GRE) {
+ fl6->fl6_gre_key = keys.keyid.keyid;
+ } else if (keys.basic.ip_proto == IPPROTO_ICMPV6) {
+ /* the default flow-keys dissector does not collect ICMP data */
+ skb_flow_get_icmp_tci(skb, &icmp, skb->data,
+ skb_transport_offset(skb),
+ skb_headlen(skb));
+ fl6->fl6_icmp_type = icmp.type;
+ fl6->fl6_icmp_code = icmp.code;
+ }
+}
+
+static struct dst_entry *ipxlat_route_output4(struct net *net,
+ struct sk_buff *skb, kuid_t uid)
+{
+ struct iphdr *iph = ip_hdr(skb);
+ bool select_icmp_source;
+ struct flowi4 fallback;
+ struct flowi4 fl4 = {
+ .daddr = iph->daddr,
+ .flowi4_dscp = inet_dsfield_to_dscp(iph->tos),
+ .flowi4_proto = iph->protocol,
+ .flowi4_mark = skb->mark,
+ .flowi4_uid = uid,
+ };
+ struct rtable *rt;
+
+ /* The engine uses a dummy IPv4 address when translating ICMPv6 errors
+ * with non-convertible source addresses. The route lookup replaces it
+ * with a local IPv4 source when possible and falls back to
+ * FLOWI_FLAG_ANYSRC otherwise.
+ */
+ select_icmp_source = iph->saddr == htonl(INADDR_DUMMY) &&
+ iph->protocol == IPPROTO_ICMP &&
+ icmp_is_err(icmp_hdr(skb)->type);
+ fl4.saddr = select_icmp_source ? 0 : iph->saddr;
+ ipxlat_flowi4_selectors(skb, &fl4);
+ fallback = fl4;
+
+ /* the translated packet has no socket, so only global XFRM applies */
+ rt = ip_route_output_flow(net, &fl4, NULL);
+ if (unlikely(select_icmp_source && (IS_ERR(rt) || !fl4.saddr))) {
+ if (!IS_ERR(rt))
+ ip_rt_put(rt);
+
+ /* If routing cannot select a local IPv4 source address, send
+ * the error from the reserved dummy address. FLOWI_FLAG_ANYSRC
+ * permits that address even though it is not assigned locally.
+ */
+ fl4 = fallback;
+ fl4.saddr = htonl(INADDR_DUMMY);
+ fl4.flowi4_flags |= FLOWI_FLAG_ANYSRC;
+ rt = ip_route_output_flow(net, &fl4, NULL);
+ }
+
+ if (IS_ERR(rt))
+ return ERR_CAST(rt);
+ if (unlikely(select_icmp_source)) {
+ iph->saddr = fl4.saddr;
+ ip_send_check(iph);
+ }
+ return &rt->dst;
+}
+
+static struct dst_entry *ipxlat_route_output6(struct net *net,
+ struct sk_buff *skb, kuid_t uid)
+{
+ struct ipv6hdr *ip6h = ipv6_hdr(skb);
+ struct dst_entry *dst;
+ struct flowi6 fl6 = {
+ .daddr = ip6h->daddr,
+ .saddr = ip6h->saddr,
+ .flowi6_proto = ip6h->nexthdr,
+ .flowi6_mark = skb->mark,
+ .flowi6_uid = uid,
+ /* translation currently leaves the Flow Label at zero, but
+ * carrying the complete flowinfo also preserves Traffic Class
+ */
+ .flowlabel = ip6_flowinfo(ip6h),
+ };
+
+ ipxlat_flowi6_selectors(skb, &fl6);
+ /* the translated packet has no socket, so only global XFRM applies */
+ dst = ip6_dst_lookup_flow(net, NULL, &fl6, NULL);
+ return dst;
+}
+
+static int ipxlat_handoff_output(struct net *net, struct sk_buff *skb)
+{
+ struct dst_entry *dst;
+ kuid_t uid;
+ int err;
+
+ /* route socketless translated output as normal kernel-originated
+ * traffic
+ */
+ uid = sock_net_uid(net, NULL);
+ if (skb->protocol == htons(ETH_P_IP))
+ dst = ipxlat_route_output4(net, skb, uid);
+ else
+ dst = ipxlat_route_output6(net, skb, uid);
+ if (IS_ERR(dst)) {
+ err = PTR_ERR(dst);
+ goto drop;
+ }
+
+ skb_dst_set(skb, dst);
+ if (skb->protocol == htons(ETH_P_IP))
+ return ip_local_out(net, NULL, skb);
+ return ip6_local_out(net, NULL, skb);
+
+drop:
+ kfree_skb(skb);
+ return err;
+}
+
+static void ipxlat_scrub_translated(struct sk_buff *skb)
+{
+ bool frag_source = false;
+ bool ignore_df = false;
+ int iif = skb->skb_iif;
+
+ if (skb->protocol == htons(ETH_P_IPV6)) {
+ ignore_df = skb->ignore_df;
+ frag_source = IP6CB(skb)->flags & IP6SKB_FRAG_SOURCE;
+ }
+
+ /* translation invalidates route, ct and other family-specific state */
+ skb_scrub_packet(skb, false);
+ skb_orphan(skb);
+ skb_set_queue_mapping(skb, 0);
+
+ /* skb->cb contains ipxlat parser state. The target-family stack reuses
+ * it as IPCB/IP6CB, so clear it and restore only the ingress identity
+ * and IPv6 fragment provenance that remain meaningful.
+ */
+ skb->skb_iif = iif;
+ if (skb->protocol == htons(ETH_P_IP)) {
+ memset(IPCB(skb), 0, sizeof(*IPCB(skb)));
+ IPCB(skb)->iif = iif;
+ } else {
+ memset(IP6CB(skb), 0, sizeof(*IP6CB(skb)));
+ IP6CB(skb)->iif = iif;
+ if (frag_source)
+ IP6CB(skb)->flags |= IP6SKB_FRAG_SOURCE;
+ }
+ skb->ignore_df = ignore_df;
+}
+
+static int ipxlat_finish_error(struct sk_buff *skb,
+ struct net_device *ingress_dev, int err)
+{
+ if (ingress_dev)
+ ipxlat_emit_icmp_error(ingress_dev, skb);
+ kfree_skb(skb);
+ return err;
+}
+
+static int ipxlat_lwt_input(struct sk_buff *skb)
+{
+ const struct lwtunnel_state *lws = skb_dst(skb)->lwtstate;
+ u8 cb_snapshot[sizeof_field(struct sk_buff, cb)];
+ struct net_device *ingress_dev = skb->dev;
+ const struct ipxlat_config *cfg;
+ struct net *net;
+ int ret;
+
+ /* in the input path, the packet must have been received by an iface */
+ if (unlikely(!ingress_dev)) {
+ kfree_skb(skb);
+ return -ENODEV;
+ }
+
+ net = dev_net(ingress_dev);
+ cfg = &ipxlat_lwt_state(lws)->config;
+
+ /* preparation owns skb->cb: preserve the receive metadata in case the
+ * translator declines the packet and the original route handles it
+ */
+ memcpy(cb_snapshot, skb->cb, sizeof(cb_snapshot));
+ ret = ipxlat_prepare(cfg, skb);
+ if (ret == -EOPNOTSUPP) {
+ /* let the stack handle unsupported packets */
+ memcpy(skb->cb, cb_snapshot, sizeof(cb_snapshot));
+ return lws->orig_input(skb);
+ }
+ if (ret)
+ return ipxlat_finish_error(skb, ingress_dev, ret);
+
+ ret = ipxlat_translate(cfg, net, skb);
+ if (ret)
+ return ipxlat_finish_error(skb, ingress_dev, ret);
+
+ ipxlat_scrub_translated(skb);
+ if (skb->protocol == htons(ETH_P_IP))
+ return ip_rcv_lwt(skb);
+ return ip6_rcv_lwt(skb);
+}
+
+static int ipxlat_lwt_output(struct net *net, struct sock *sk,
+ struct sk_buff *skb)
+{
+ struct net_device *ingress_dev = skb->skb_iif ? skb->dev : NULL;
+ u8 cb_snapshot[sizeof_field(struct sk_buff, cb)];
+ const struct dst_entry *dst = skb_dst(skb);
+ struct lwtunnel_state *lws = dst->lwtstate;
+ const struct ipxlat_config *cfg;
+ int ret;
+
+ cfg = &ipxlat_lwt_state(lws)->config;
+
+ /* preparation owns skb->cb: preserve the receive metadata in case the
+ * translator declines the packet and the original route handles it
+ */
+ memcpy(cb_snapshot, skb->cb, sizeof(cb_snapshot));
+ ret = ipxlat_prepare(cfg, skb);
+ if (ret == -EOPNOTSUPP) {
+ /* let the stack handle unsupported packets */
+ memcpy(skb->cb, cb_snapshot, sizeof(cb_snapshot));
+ return lws->orig_output(net, sk, skb);
+ }
+ if (ret)
+ return ipxlat_finish_error(skb, ingress_dev, ret);
+
+ ret = ipxlat_translate(cfg, net, skb);
+ if (ret)
+ return ipxlat_finish_error(skb, ingress_dev, ret);
+
+ ipxlat_scrub_translated(skb);
+ return ipxlat_handoff_output(net, skb);
+}
+
+static const struct lwtunnel_encap_ops ipxlat_lwt_ops = {
+ .build_state = ipxlat_build_state,
+ .output = ipxlat_lwt_output,
+ .input = ipxlat_lwt_input,
+ .fill_encap = ipxlat_fill_encap,
+ .get_encap_size = ipxlat_get_encap_size,
+ .cmp_encap = ipxlat_cmp_encap,
+ .owner = THIS_MODULE,
+};
+
+static int __init ipxlat_lwt_init(void)
+{
+ return lwtunnel_encap_add_ops(&ipxlat_lwt_ops, LWTUNNEL_ENCAP_IPXLAT);
+}
+module_init(ipxlat_lwt_init);
+
+static void __exit ipxlat_lwt_exit(void)
+{
+ lwtunnel_encap_del_ops(&ipxlat_lwt_ops, LWTUNNEL_ENCAP_IPXLAT);
+}
+module_exit(ipxlat_lwt_exit);
+
+MODULE_ALIAS_RTNL_LWT(IPXLAT);
+MODULE_DESCRIPTION("IPv6<>IPv4 translation lightweight tunnel (SIIT)");
+MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 17/18] selftests: net: add ipxlat coverage
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (15 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 16/18] net: ipxlat: add lightweight tunnel attachment Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 18/18] docs: networking: add ipxlat guide Ralf Lici
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Daniel Gröber, Antonio Quartulli, Shuah Khan,
linux-kselftest, linux-kernel
Exercise forwarded and local translation, native fallback, target-family
conntrack and NAT, checksumless UDP, route-PMTU fragmentation, conntrack
reassembly followed by refragmentation, PMTU feedback and ICMP errors.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-15-ralf@mandelbit.com/
- Replace the v1 netdevice/YNL suite with an iproute2/LWT namespace
topology.
- Add coverage for local and forwarded LWT translation, native fallback,
translated-family conntrack and masquerade, route-PMTU fragmentation,
reassembly/refragmentation, and PMTU feedback.
- Verify zero-checksum UDPv4 at a real translated-family receiver
instead of treating a packet capture as proof of successful delivery.
(Sashiko)
- Keep forwarding, sysctls, routes, and translator state inside
disposable network namespaces so a failed test cannot leave host
networking changed. (Sashiko)
- Remove the dedicated UDP generator and produce the required packet
shapes with existing selftest tools, avoiding the v1 helper's
undeclared padding bytes. Remove the old fragmented-ICMP capture
filter whose fixed offset could miss ICMP after an IPv6 Fragment
Header. (Sashiko)
tools/testing/selftests/Makefile | 1 +
tools/testing/selftests/net/ipxlat/Makefile | 5 +
tools/testing/selftests/net/ipxlat/config | 13 +
tools/testing/selftests/net/ipxlat/ipxlat.sh | 554 +++++++++++++++++++
4 files changed, 573 insertions(+)
create mode 100644 tools/testing/selftests/net/ipxlat/Makefile
create mode 100644 tools/testing/selftests/net/ipxlat/config
create mode 100755 tools/testing/selftests/net/ipxlat/ipxlat.sh
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 1198e570898d..339a74d509b8 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -90,6 +90,7 @@ TARGETS += net/af_unix
TARGETS += net/can
TARGETS += net/forwarding
TARGETS += net/hsr
+TARGETS += net/ipxlat
TARGETS += net/mptcp
TARGETS += net/netfilter
TARGETS += net/openvswitch
diff --git a/tools/testing/selftests/net/ipxlat/Makefile b/tools/testing/selftests/net/ipxlat/Makefile
new file mode 100644
index 000000000000..d54a89a84337
--- /dev/null
+++ b/tools/testing/selftests/net/ipxlat/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_PROGS := ipxlat.sh
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/net/ipxlat/config b/tools/testing/selftests/net/ipxlat/config
new file mode 100644
index 000000000000..ced834911830
--- /dev/null
+++ b/tools/testing/selftests/net/ipxlat/config
@@ -0,0 +1,13 @@
+CONFIG_INET=y
+CONFIG_IPV6=y
+CONFIG_LWTUNNEL=y
+CONFIG_NET_IPXLAT=m
+CONFIG_NET_IPXLAT_LWT=m
+CONFIG_NET_NS=y
+CONFIG_NF_CONNTRACK=m
+CONFIG_NF_NAT=m
+CONFIG_NF_TABLES=m
+CONFIG_NF_TABLES_IPV4=y
+CONFIG_NF_TABLES_IPV6=y
+CONFIG_NFT_NAT=m
+CONFIG_VETH=m
diff --git a/tools/testing/selftests/net/ipxlat/ipxlat.sh b/tools/testing/selftests/net/ipxlat/ipxlat.sh
new file mode 100755
index 000000000000..955a50ac8231
--- /dev/null
+++ b/tools/testing/selftests/net/ipxlat/ipxlat.sh
@@ -0,0 +1,554 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# IPXLAT lightweight-tunnel selftests
+#
+# Copyright (C) 2026- Mandelbit SRL
+# Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+#
+# Author: Antonio Quartulli <antonio@mandelbit.com>
+# Daniel Gröber <dxld@darkboxed.org>
+# Ralf Lici <ralf@mandelbit.com>
+#
+# ipxlat_ns4 (IPv4 host) --- ipxlat_nsx --- ipxlat_ns6 (IPv6 host)
+
+set -o pipefail
+
+TEST_DIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+readonly TEST_DIR
+source "$TEST_DIR/../lib.sh"
+
+# All addresses come from documentation ranges. The EAM replaces
+# 198.51.100.0/24 with 2001:db8:200::/120 and preserves only the final host
+# byte. V4_XLAT therefore becomes V6_EAM_XLAT, while V4_NEAR becomes
+# V6_EAM_NEAR. EAM takes precedence in both directions.
+#
+# Addresses outside the EAM use the RFC 6052 fallback. With a /96 prefix it
+# appends the complete 32-bit IPv4 address, so V4_REMOTE and V4_PMTU become
+# V6_REMOTE and V6_PMTU. V6_NATIVE is deliberately outside both mappings and
+# exists only to exercise native output when ipxlat declines a packet.
+#
+# nsx also needs an IPv6 gateway address. We deliberately choose V6_XLAT as
+# the RFC 6052 form of V4_XLAT so that, if the kernel uses it as the source of
+# an ICMPv6 error, translation produces the meaningful V4_XLAT source instead
+# of the 192.0.0.8 dummy fallback. V6_XLAT is not the normal 4-to-6 result for
+# V4_XLAT: EAM still produces V6_EAM_XLAT. Both IPv6 addresses map back to
+# V4_XLAT, each through its respective algorithm.
+
+readonly V4_NEAR_PREFIX=198.51.100.0/24
+readonly V4_XLAT=198.51.100.1
+readonly V4_NEAR=198.51.100.2
+readonly V4_REMOTE_PREFIX=192.0.2.0/24
+readonly V4_REMOTE=192.0.2.33
+readonly V4_PMTU=192.0.2.34
+
+readonly V6_RFC6052_PREFIX=2001:db8:100::/96
+readonly V6_XLAT=2001:db8:100::c633:6401
+readonly V6_REMOTE=2001:db8:100::c000:221
+readonly V6_PMTU=2001:db8:100::c000:222
+readonly V6_NATIVE=2001:db8:100:0:1::1
+readonly V6_EAM_PREFIX=2001:db8:200::/120
+readonly V6_EAM_XLAT=2001:db8:200::1
+readonly V6_EAM_NEAR=2001:db8:200::2
+
+readonly IPERF_PORT=5201
+readonly UDP_PORT=5202
+
+BG_PIDS=()
+TMP_FILES=()
+
+# setup_ns assigns these variables indirectly
+ipxlat_ns4=
+ipxlat_nsx=
+ipxlat_ns6=
+
+cleanup()
+{
+ local file pid
+
+ for pid in "${BG_PIDS[@]}"; do
+ kill "$pid" 2>/dev/null || true
+ wait "$pid" 2>/dev/null || true
+ done
+ for file in "${TMP_FILES[@]}"; do
+ rm -f "$file"
+ done
+ cleanup_all_ns
+}
+
+counter_nonzero()
+{
+ local packets
+ local ns=$1
+ local family=$2
+ local table=$3
+ local counter=$4
+
+ packets=$(ip netns exec "$ns" nft list counter \
+ "$family" "$table" "$counter" |
+ awk '/packets/ { print $2; exit }') || return 1
+ ((packets > 0))
+}
+
+tcp_listener_ready()
+{
+ [[ -n $(ip netns exec "$ipxlat_ns4" ss -Hln \
+ "sport = :$IPERF_PORT" 2>/dev/null) ]]
+}
+
+udp_listener_ready()
+{
+ [[ -n $(ip netns exec "$ipxlat_ns6" ss -Hlun \
+ "sport = :$UDP_PORT" 2>/dev/null) ]]
+}
+
+file_has_size()
+{
+ local file=$1
+ local expected=$2
+
+ [[ -f $file ]] && [[ $(stat -c %s "$file") -eq $expected ]]
+}
+
+pmtu_is_1260()
+{
+ ip -n "$ipxlat_ns4" route get "$V4_PMTU" |
+ grep -Eq 'mtu[[:space:]]+1260'
+}
+
+setup_topology()
+{
+ # The IPv4-only host reaches the IPv6-only host through the translator.
+ # EAM gives IPv6 identities to the IPv4 endpoints, while RFC 6052 gives
+ # IPv4 identities to the IPv6 endpoints. The test-only V6_NATIVE
+ # address is omitted from the operational topology below.
+ #
+ # Topology:
+ # +------------------------------------------------------------------+
+ # | IPXLAT_NS4 |
+ # | |
+ # | +------------+ |
+ # | v4 | V4_NEAR/24 | |
+ # +------------------------------------------------------------------+
+ # |
+ # |
+ # +------------------------------------------------------------------+
+ # | x4 | V4_XLAT/24 | |
+ # | +------------+ |
+ # | |
+ # | IPXLAT_NSX |
+ # | |
+ # | +-----------------+ |
+ # | | V6_XLAT/64 | |
+ # | x6 | V6_EAM_XLAT/128 | |
+ # +------------------------------------------------------------------+
+ # |
+ # |
+ # +------------------------------------------------------------------+
+ # | v6 | V6_REMOTE/64 | |
+ # | | V6_PMTU/128 | |
+ # | | V6_NATIVE/128 | |
+ # | +-----------------+ |
+ # | |
+ # | IPXLAT_NS6 |
+ # +------------------------------------------------------------------+
+
+ ip link add v4 type veth peer name x4
+ ip link set v4 netns "$ipxlat_ns4"
+ ip link set x4 netns "$ipxlat_nsx"
+ ip link add v6 type veth peer name x6
+ ip link set v6 netns "$ipxlat_ns6"
+ ip link set x6 netns "$ipxlat_nsx"
+
+ ip -n "$ipxlat_ns4" addr add "$V4_NEAR/24" dev v4
+ ip -n "$ipxlat_nsx" addr add "$V4_XLAT/24" dev x4
+
+ # These are selftest controls, not ipxlat requirements: nodad avoids
+ # asynchronous address setup, while noprefixroute keeps the EAM address
+ # from creating a main-table route that would bypass the tested route.
+ ip -n "$ipxlat_nsx" -6 addr add "$V6_XLAT/64" dev x6 nodad
+ ip -n "$ipxlat_nsx" -6 addr add "$V6_EAM_XLAT/128" dev x6 nodad \
+ noprefixroute
+ ip -n "$ipxlat_ns6" -6 addr add "$V6_REMOTE/64" dev v6 nodad
+ ip -n "$ipxlat_ns6" -6 addr add "$V6_PMTU/128" dev v6 nodad
+ # avoid waiting for DAD on the isolated fallback-test address
+ ip -n "$ipxlat_ns6" -6 addr add "$V6_NATIVE/128" dev v6 nodad
+
+ ip -n "$ipxlat_ns4" link set v4 up
+ ip -n "$ipxlat_nsx" link set x4 up
+ ip -n "$ipxlat_nsx" link set x6 up
+ ip -n "$ipxlat_ns6" link set v6 up
+
+ ip netns exec "$ipxlat_nsx" sysctl -qw net.ipv4.ip_forward=1
+ ip netns exec "$ipxlat_nsx" sysctl -qw net.ipv6.conf.all.forwarding=1
+
+ ip -n "$ipxlat_ns4" route add default via "$V4_XLAT"
+ # explicitly use the ordinary endpoint for non-PMTU traffic since ns6
+ # has two addresses
+ ip -n "$ipxlat_ns6" -6 route add "$V6_EAM_PREFIX" via "$V6_XLAT" \
+ src "$V6_REMOTE"
+
+ # constrain only the PMTU endpoint while all other traffic uses the
+ # normal 1500-byte veth MTU
+ ip -n "$ipxlat_nsx" -6 route add "$V6_PMTU/128" dev x6 mtu 1280
+
+ ip -n "$ipxlat_nsx" route add "$V4_REMOTE_PREFIX" \
+ encap ipxlat rfc6052-prefix6 "$V6_RFC6052_PREFIX" \
+ eam "$V4_NEAR_PREFIX" "$V6_EAM_PREFIX" dev x6
+
+ ip -n "$ipxlat_nsx" -6 route add "$V6_EAM_PREFIX" \
+ encap ipxlat rfc6052-prefix6 "$V6_RFC6052_PREFIX" \
+ eam "$V4_NEAR_PREFIX" "$V6_EAM_PREFIX" dev x4
+ # address assignment creates a metric-zero kernel local route so remove
+ # it before adding the LWT
+ ip -n "$ipxlat_nsx" -6 route del local "$V6_EAM_XLAT/128" table local
+ ip -n "$ipxlat_nsx" -6 route add local "$V6_EAM_XLAT/128" table local \
+ encap ipxlat rfc6052-prefix6 "$V6_RFC6052_PREFIX" \
+ eam "$V4_NEAR_PREFIX" "$V6_EAM_PREFIX" dev x6
+}
+
+warm_up_neighbors()
+{
+ # keep first-packet ARP/NDP delays out of the translation assertions
+ ip netns exec "$ipxlat_nsx" ping -c 1 -W 2 "$V4_NEAR" \
+ >/dev/null 2>&1
+ ip netns exec "$ipxlat_nsx" ping -6 -c 1 -W 2 "$V6_REMOTE" \
+ >/dev/null 2>&1
+}
+
+setup_refragment_nft()
+{
+ # Enable conntrack only for IPv6 here. If IPv4 conntrack were already
+ # active, it would reassemble the source frags before ipxlat saw them.
+ ip netns exec "$ipxlat_nsx" nft -f - <<EOF
+add table ip6 ipxlat_refragment
+add counter ip6 ipxlat_refragment tracked_udp
+add chain ip6 ipxlat_refragment postrouting { type filter hook postrouting \
+priority filter; policy accept; }
+add rule ip6 ipxlat_refragment postrouting ip6 saddr $V6_EAM_NEAR \
+ip6 daddr $V6_PMTU udp dport $UDP_PORT ct state { new, established } \
+counter name tracked_udp
+EOF
+}
+
+setup_nft()
+{
+ # count the translated IPv4 errors returned to the IPv4 host
+ ip netns exec "$ipxlat_ns4" nft -f - <<EOF
+add table ip ipxlat_test
+add counter ip ipxlat_test frag_needed
+add counter ip ipxlat_test port_unreachable
+add chain ip ipxlat_test input { type filter hook input priority filter; policy accept; }
+add rule ip ipxlat_test input icmp type destination-unreachable icmp code frag-needed counter name frag_needed
+add rule ip ipxlat_test input icmp type destination-unreachable icmp code port-unreachable counter name port_unreachable
+EOF
+
+ # count the translated IPv6 error returned to the IPv6 host
+ ip netns exec "$ipxlat_ns6" nft -f - <<EOF
+add table ip6 ipxlat_test
+add counter ip6 ipxlat_test port_unreachable
+add chain ip6 ipxlat_test input { type filter hook input priority filter; policy accept; }
+add rule ip6 ipxlat_test input icmpv6 type destination-unreachable icmpv6 code port-unreachable counter name port_unreachable
+EOF
+
+ # count locally translated packets that reach IPv6 LOCAL_OUT
+ ip netns exec "$ipxlat_nsx" nft -f - <<EOF
+add table ip6 ipxlat_output
+add counter ip6 ipxlat_output translated_echo
+add chain ip6 ipxlat_output output { type filter hook output priority filter; policy accept; }
+add rule ip6 ipxlat_output output ip6 saddr $V6_EAM_XLAT ip6 daddr $V6_REMOTE icmpv6 type echo-request counter name translated_echo
+EOF
+}
+
+setup_nat64_nft()
+{
+ # MASQUERADE proves that translated IPv4 traffic reaches conntrack and
+ # POSTROUTING. Install it after the refragmentation test because enabling
+ # IPv4 conntrack earlier would reassemble its source fragments.
+ ip netns exec "$ipxlat_nsx" nft -f - <<EOF
+add table ip ipxlat_nat
+add counter ip ipxlat_nat translated_tcp
+add chain ip ipxlat_nat postrouting { type nat hook postrouting priority srcnat; policy accept; }
+add rule ip ipxlat_nat postrouting ip saddr $V4_REMOTE ip daddr $V4_NEAR tcp dport $IPERF_PORT counter name translated_tcp masquerade
+EOF
+}
+
+# translate a forwarded IPv4 echo request to IPv6 and its reply back to IPv4
+test_forwarded_echo()
+{
+ RET=0
+ ip netns exec "$ipxlat_ns4" ping -c 1 -W 2 "$V4_REMOTE" \
+ >/dev/null 2>&1
+ check_err $? "IPv4 echo did not cross the translator"
+ log_test "forwarded ICMP translation"
+}
+
+# require locally generated IPv4 traffic to pass through IPv6 LOCAL_OUT
+test_local_output()
+{
+ RET=0
+ ip netns exec "$ipxlat_nsx" ping -I "$V4_XLAT" -c 1 -W 2 \
+ "$V4_REMOTE" >/dev/null 2>&1
+ check_err $? "locally originated IPv4 echo failed"
+ slowwait 2 counter_nonzero "$ipxlat_nsx" ip6 ipxlat_output \
+ translated_echo
+ check_err $? "translated packet did not traverse IPv6 LOCAL_OUT"
+ log_test "locally originated CLAT traffic"
+}
+
+# return valid packets unsupported by SIIT to the original route
+test_native_fallback()
+{
+ RET=0
+
+ # Test native input fallback
+
+ # remove the cached ethernet address for V6_REMOTE from nsx so that the
+ # next packet must perform IPv6 neighbor discovery
+ ip -n "$ipxlat_nsx" -6 neigh flush to "$V6_REMOTE" dev x6 \
+ >/dev/null 2>&1
+
+ # The sequence here is:
+ # 1. nsx sends a neighbor solicitation;
+ # 2. ns6 replies with a neighbor advertisement;
+ # 3. because the solicitation used V6_EAM_XLAT as its source, the
+ # advertisement is addressed to V6_EAM_XLAT (the local route for
+ # V6_XLAT is an ordinary IPv6 route, it does not carry ipxlat);
+ # 4. the local route for V6_EAM_XLAT carries the ipxlat input LWT, so
+ # the advertisement enters ipxlat_lwt_input;
+ # 5. both NA endpoints are translatable, but the ICMPv6 type is not so
+ # the input cb restores the original metadata and calls orig_input;
+ # 6. normal IPv6 neighbor discovery then consumes the unchanged
+ # advertisement and updates the neighbor table;
+ # 7. the ping itself is allowed to fail because its echo reply is
+ # addressed to V6_EAM_XLAT and, unlike the NA, an echo reply is
+ # translatable, so the input LWT translates it to IPv4 instead of
+ # delivering it to the IPv6 ping socket.
+ ip netns exec "$ipxlat_nsx" ping -6 -I "$V6_EAM_XLAT" \
+ -c 1 -W 1 "$V6_REMOTE" >/dev/null 2>&1 || true
+
+ # if the neighbor entry exists in one of these usable states, the NA
+ # must have passed through native IPv6 input (not swallowed by ipxlat)
+ ip -n "$ipxlat_nsx" -6 neigh show "$V6_REMOTE" dev x6 |
+ grep -Eq '\b(REACHABLE|STALE|DELAY|PROBE)\b'
+ check_err $? "Neighbor Advertisement was intercepted by the local LWT"
+
+ # Test native output fallback
+
+ # V6_XLAT was already reachable through the connected /64, but this
+ # /128 route takes precedence and forces locally generated traffic for
+ # that address through ipxlat_lwt_output
+ ip -n "$ipxlat_ns6" -6 route add "$V6_XLAT/128" \
+ encap ipxlat rfc6052-prefix6 "$V6_RFC6052_PREFIX" \
+ eam "$V4_NEAR_PREFIX" "$V6_EAM_PREFIX" dev v6
+ check_err $? "could not install native-output fallback route"
+
+ # The source is outside both mappings, so native output must send the
+ # unchanged packet across the existing IPv6 link.
+ #
+ # The sequence is:
+ # 1. ipxlat_prepare validates the IPv6 packet returns -EOPNOTSUPP
+ # because it cannot convert V6_NATIVE;
+ # 2. the output callback restores the skb metadata and calls the
+ # route's saved orig_output callback;
+ # 3. native IPv6 sends the echo request over the existing veth link.
+ #
+ # nsx owns V6_XLAT, whose normal local route does not carry ipxlat, so
+ # it answers normally. The reply returns to the ordinary local route
+ # for V6_NATIVE, and the ping succeeds.
+ ip netns exec "$ipxlat_ns6" ping -6 -I "$V6_NATIVE" \
+ -c 1 -W 1 "$V6_XLAT" >/dev/null 2>&1
+ check_err $? "untranslatable packet did not cross native output"
+ ip -n "$ipxlat_ns6" -6 route del "$V6_XLAT/128" >/dev/null 2>&1 || true
+ log_test "native input and output fallback"
+}
+
+# run IPv6-initiated TCP through translation and target-family MASQUERADE
+test_stateful_nat64()
+{
+ local server_pid
+
+ RET=0
+ timeout 10 ip netns exec "$ipxlat_ns4" iperf3 -s -1 -p "$IPERF_PORT" \
+ >/dev/null 2>&1 &
+ server_pid=$!
+ BG_PIDS+=("$server_pid")
+ slowwait 2 tcp_listener_ready
+ check_err $? "iperf3 server did not start"
+
+ timeout 10 ip netns exec "$ipxlat_ns6" iperf3 -6 \
+ -c "$V6_EAM_NEAR" -B "$V6_REMOTE" -p "$IPERF_PORT" \
+ -n 256K >/dev/null 2>&1
+ check_err $? "TCP transfer through stateful NAT64 failed"
+ wait "$server_pid"
+ check_err $? "iperf3 server failed"
+
+ slowwait 2 counter_nonzero "$ipxlat_nsx" ip ipxlat_nat translated_tcp
+ check_err $? "translated IPv4 traffic did not reach MASQUERADE"
+ log_test "target-family conntrack and NAT"
+}
+
+# require a large checksumless IPv4 UDP datagram to survive IPv6 fragmentation
+test_zero_checksum_postfragment()
+{
+ local target="UDP4-DATAGRAM:$V4_PMTU:$UDP_PORT"
+ local payload_len=1400
+ local receiver_pid
+ local output
+
+ RET=0
+ output=$(mktemp)
+ TMP_FILES+=("$output")
+
+ timeout 4 ip netns exec "$ipxlat_ns6" socat -u \
+ "UDP6-RECVFROM:$UDP_PORT,reuseaddr" \
+ "CREATE:$output" >/dev/null 2>&1 &
+ receiver_pid=$!
+ BG_PIDS+=("$receiver_pid")
+ slowwait 2 udp_listener_ready
+ check_err $? "IPv6 UDP receiver did not start"
+
+ head -c "$payload_len" /dev/zero |
+ ip netns exec "$ipxlat_ns4" socat -u - \
+ "$target,so-no-check=1,ip-mtu-discover=0"
+ # SO_NO_CHECK creates a real CHECKSUM_NONE packet and PMTUDISC_DONT
+ # leaves DF clear, so the 1280-byte translated route must fragment the
+ # complete IPv6 datagram.
+ check_err $? "checksumless IPv4 UDP send failed"
+ slowwait 3 file_has_size "$output" "$payload_len"
+ check_err $? "translated IPv6 UDP datagram was not delivered intact"
+
+ kill "$receiver_pid" 2>/dev/null || true
+ wait "$receiver_pid" 2>/dev/null || true
+ log_test "UDP zero checksum and route-PMTU fragmentation"
+}
+
+# preserve local fragmentation ownership across target-family conntrack defrag
+test_conntrack_refragment()
+{
+ local target="UDP4-DATAGRAM:$V4_PMTU:$UDP_PORT"
+ local payload_len=1900
+ local receiver_pid
+ local output
+
+ RET=0
+ output=$(mktemp)
+ TMP_FILES+=("$output")
+
+ timeout 4 ip netns exec "$ipxlat_ns6" socat -u \
+ "UDP6-RECVFROM:$UDP_PORT,reuseaddr" \
+ "CREATE:$output" >/dev/null 2>&1 &
+ receiver_pid=$!
+ BG_PIDS+=("$receiver_pid")
+ slowwait 2 udp_listener_ready
+ check_err $? "IPv6 UDP receiver did not start"
+
+ # The IPv4 sender first splits the datagram at 1400 bytes. Translation
+ # produces IPv6 fragments larger than the 1280-byte target route. IPv6
+ # conntrack reassembles them in LOCAL_OUT, then IPv6 output must
+ # fragment the complete datagram again for that route.
+ ip -n "$ipxlat_ns4" link set v4 mtu 1400
+ head -c "$payload_len" /dev/zero |
+ ip netns exec "$ipxlat_ns4" socat -u - \
+ "$target,ip-mtu-discover=0"
+ check_err $? "fragmented IPv4 UDP send failed"
+ ip -n "$ipxlat_ns4" link set v4 mtu 1500
+
+ slowwait 3 file_has_size "$output" "$payload_len"
+ check_err $? "conntrack-defragmented IPv6 datagram was not delivered"
+ slowwait 2 counter_nonzero "$ipxlat_nsx" ip6 ipxlat_refragment \
+ tracked_udp
+ check_err $? "reassembled IPv6 datagram did not acquire conntrack state"
+
+ kill "$receiver_pid" 2>/dev/null || true
+ wait "$receiver_pid" 2>/dev/null || true
+ log_test "conntrack defrag and target-route refragmentation"
+}
+
+# translate IPv6 PTB feedback and install the resulting IPv4 PMTU exception
+test_pmtu_feedback()
+{
+ RET=0
+ ip netns exec "$ipxlat_ns4" ping -M "do" -s 1300 -c 1 -W 1 \
+ "$V4_PMTU" >/dev/null 2>&1 || true
+
+ slowwait 3 counter_nonzero "$ipxlat_ns4" ip ipxlat_test frag_needed
+ check_err $? "translated IPv4 Fragmentation Needed was not received"
+ slowwait 3 pmtu_is_1260
+ check_err $? "IPv4 PMTU was not updated to 1260"
+ log_test "translated-family PMTU feedback"
+}
+
+# translate an IPv6 Port Unreachable response back to the IPv4 sender
+test_icmp_error_46()
+{
+ RET=0
+ printf x | ip netns exec "$ipxlat_ns4" socat -u - \
+ "UDP4-DATAGRAM:$V4_REMOTE:$UDP_PORT" >/dev/null 2>&1
+ check_err $? "IPv4 UDP trigger failed"
+ slowwait 3 counter_nonzero "$ipxlat_ns4" ip ipxlat_test \
+ port_unreachable
+ check_err $? "translated IPv4 Port Unreachable was not received"
+ log_test "ICMP error translation 4 to 6"
+}
+
+# translate an IPv4 Port Unreachable response back to the IPv6 sender
+test_icmp_error_64()
+{
+ RET=0
+ printf x | ip netns exec "$ipxlat_ns6" socat -u - \
+ "UDP6-DATAGRAM:[$V6_EAM_NEAR]:$UDP_PORT" >/dev/null 2>&1
+ check_err $? "IPv6 UDP trigger failed"
+ slowwait 3 counter_nonzero "$ipxlat_ns6" ip6 ipxlat_test \
+ port_unreachable
+ check_err $? "translated IPv6 Port Unreachable was not received"
+ log_test "ICMP error translation 6 to 4"
+}
+
+if [[ $(id -u) -ne 0 ]]; then
+ echo "SKIP: this test needs root privileges"
+ exit "$ksft_skip"
+fi
+
+require_command ip
+require_command iperf3
+require_command nft
+require_command ping
+require_command socat
+require_command ss
+require_command timeout
+
+if ! grep -qw ipxlat < <(ip route help 2>&1); then
+ echo "SKIP: iproute2 does not support the ipxlat LWT encapsulation"
+ exit "$ksft_skip"
+fi
+
+trap cleanup EXIT
+
+set -e
+setup_ns ipxlat_ns4 ipxlat_nsx ipxlat_ns6
+setup_topology
+warm_up_neighbors
+setup_nft
+
+# test_conntrack_refragment requires setup_refragment_nft
+setup_refragment_nft
+set +e
+test_conntrack_refragment
+ip netns exec "$ipxlat_nsx" nft delete table ip6 ipxlat_refragment
+
+# the rest of the tests require setup_nat64_nft
+set -e
+setup_nat64_nft
+set +e
+
+test_forwarded_echo
+test_local_output
+test_native_fallback
+test_stateful_nat64
+test_zero_checksum_postfragment
+test_pmtu_feedback
+test_icmp_error_46
+test_icmp_error_64
+
+exit "$EXIT_STATUS"
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread* [RFC net-next v2 18/18] docs: networking: add ipxlat guide
2026-10-24 9:25 [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation Ralf Lici
` (16 preceding siblings ...)
2026-10-24 9:25 ` [RFC net-next v2 17/18] selftests: net: add ipxlat coverage Ralf Lici
@ 2026-10-24 9:25 ` Ralf Lici
17 siblings, 0 replies; 19+ messages in thread
From: Ralf Lici @ 2026-10-24 9:25 UTC (permalink / raw)
To: netdev
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Daniel Gröber, Antonio Quartulli,
Jonathan Corbet, Shuah Khan, Randy Dunlap,
Toke Høiland-Jørgensen, Beniamino Galvani,
Xavier Hsinyuan, linux-doc, linux-kernel
Document the route configuration model, address mapping, CLAT and
NAT64 examples, packet continuation, conntrack, fragmentation,
checksum handling and operational limitations.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-16-ralf@mandelbit.com/
- Rewrite the guide for the LWT route model and inline immutable
configuration; remove netdevice, generic-netlink/YNL, device MTU, and
device statistics documentation and the confusing receive-reinjection
model. (Toke)
- Document EAM-before-RFC6052 endpoint conversion, both CLAT addressing
models, and downstream source validation outside the engine.
(Beniamino)
- Add end-to-end single-host CLAT, delegated-prefix CLAT, stateful NAT64,
464XLAT, and SIIT border-router examples.
- Add short /96 examples which show both address-translation directions.
(Xavier)
- Replace the broken v1 YNL example with iproute2 commands for LWT route
configuration. (Xavier)
- Add the guide to Documentation/networking/index.rst. (Jonathan)
- Fix omitted route commands, inconsistent variable names, spelling, and
RST problems in the v1 guide. (Sashiko)
- Document native fallback, family-switch continuation, conntrack/NAT,
XFRM, TTL/Hop Limit, target-route fragmentation, checksumless UDP, and
protocol handling.
Documentation/networking/index.rst | 1 +
Documentation/networking/ipxlat.rst | 333 ++++++++++++++++++++++++++++
net/ipxlat/Kconfig | 2 +
3 files changed, 336 insertions(+)
create mode 100644 Documentation/networking/ipxlat.rst
diff --git a/Documentation/networking/index.rst b/Documentation/networking/index.rst
index 44a422ad3b05..5f5e2aa61d3c 100644
--- a/Documentation/networking/index.rst
+++ b/Documentation/networking/index.rst
@@ -66,6 +66,7 @@ Contents:
ipv6
ipvlan
ipvs-sysctl
+ ipxlat
kcm
l2tp
lapb-module
diff --git a/Documentation/networking/ipxlat.rst b/Documentation/networking/ipxlat.rst
new file mode 100644
index 000000000000..50d5447e18b3
--- /dev/null
+++ b/Documentation/networking/ipxlat.rst
@@ -0,0 +1,333 @@
+.. SPDX-License-Identifier: GPL-2.0+
+.. Copyright (C) 2026- Mandelbit SRL
+.. Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org>
+.. Author: Antonio Quartulli <antonio@mandelbit.com>
+.. Author: Daniel Gröber <dxld@darkboxed.org>
+.. Author: Ralf Lici <ralf@mandelbit.com>
+
+======
+IPXLAT
+======
+
+ipxlat translates IPv4 packets into IPv6 packets and IPv6 packets into IPv4
+packets. It replaces the IP header, maps the source and destination addresses,
+updates transport checksums, and translates ICMP messages and quoted packets.
+It also handles the fragment and offload details needed to preserve the packet
+across the family change. This is a building block for establishing layer 3
+connectivity between otherwise uncommunicative IPv6-only and/or IPv4-only
+networks.
+
+The translation is stateless: the result is determined by the packet and the
+configured address mappings while transport ports are not translated. This
+implements the SIIT algorithm from RFC 7915, RFC 6052 address embedding, and
+optional RFC 7757 Explicit Address Mappings.
+
+The implementation consists of a reusable translation engine and a
+lightweight-tunnel (LWT) attachment. A route carrying the ``ipxlat``
+encapsulation selects packets for translation. The engine is controlled by
+``CONFIG_NET_IPXLAT`` and the LWT attachment by ``CONFIG_NET_IPXLAT_LWT``.
+
+The family before translation is referred to below as the original family and
+the family after translation as the translated family. Received packets enter
+the translated family's ``PRE_ROUTING`` hook and undergo a new route lookup.
+Locally originated packets enter the translated family's ``LOCAL_OUT`` path
+without carrying the original socket. Translated-family routing and XFRM
+therefore use the kernel-originated UID and global policy; socket, cgroup, and
+per-socket XFRM identity do not cross the family switch. If a selected packet
+is not supported by the translator, the LWT passes it back to the route's
+normal input or output path unchanged. Malformed packets and failures
+encountered during translation are dropped.
+
+Configuration
+=============
+
+Each route has one immutable configuration:
+
+* ``eam`` configures an optional RFC 7757 Explicit Address Mapping. It is
+ encoded as one element of the ``EAMT`` nested list, which accepts at most one
+ element. The EAM contains one canonical IPv4 prefix and one canonical IPv6
+ prefix. For an IPv4 prefix length ``M`` and an IPv6 prefix length ``N``, the
+ IPv6 prefix must have room for every IPv4 host bit::
+
+ 128 - N >= 32 - M
+
+ Equivalently, ``N`` must be no greater than ``M + 96``. Equality gives both
+ prefixes the same number of host addresses. A shorter IPv6 prefix is also
+ accepted, but additional IPv6 host bits have no IPv4 representation and are
+ discarded during 6-to-4 translation. At most one EAM is supported per route,
+ and RFC 7757 hairpinning is not implemented.
+* ``rfc6052-prefix6`` is the required RFC 6052 fallback prefix. It is used for
+ every address which does not match the EAM, so it normally identifies the
+ far-side NAT64 service in a CLAT configuration. It must have length /32,
+ /40, /48, /56, /64, or /96. Non-canonical host bits are normalized in the
+ route state and in dumps, and the reserved octet at bits 64-71 must be zero.
+
+Each source and destination address is translated independently. If an address
+matches the EAM in its original family, the paired EAM prefix is used. An
+address which does not match the EAM is translated with ``rfc6052-prefix6``.
+This lets one configuration use an explicit mapping for the nearby IPv4 host
+or network while using an RFC 6052 prefix for remote IPv4 addresses. Without
+an EAM every endpoint uses the symmetric RFC 6052 mapping.
+
+There is one exception for ICMP errors. An IPv6 router can generate an ICMPv6
+error from an address outside both configured mappings. If the destination is
+translatable, the engine represents the source with the RFC 7600 dummy IPv4
+address ``192.0.0.8``. Before local output, the LWT attachment asks the IPv4
+route to select a real local source address. If routing cannot select one, it
+falls back to ``192.0.0.8`` and permits that reserved, unassigned source for
+this error only. This use of the dummy address for an untranslatable ICMPv6
+source is described by ``draft-ietf-v6ops-icmpext-xlat-v6only-source``. The
+dummy address is never used for an ordinary packet or for an untranslatable
+destination.
+
+ipxlat does not add the Node Identification ICMP extension proposed by that
+draft. Diagnostic tools may consequently show the selected IPv4 source, or
+``192.0.0.8`` when routing cannot select one, instead of the IPv6 address of
+the router which generated the error.
+
+ipxlat validates the address format but leaves prefix selection to the
+operator. RFC 6052 restricts the well-known prefix ``64:ff9b::/96`` to global
+IPv4 addresses. Local-use deployments can use ``64:ff9b:1::/48`` or a
+more-specific prefix as described by RFC 8215, or use a network-specific
+prefix. The examples below use documentation addresses only.
+
+The companion iproute2 support uses the following grammar (the kernel UAPI is
+in ``include/uapi/linux/ipxlat.h``)::
+
+ encap ipxlat rfc6052-prefix6 PREFIX/LEN
+ [eam IPV4_PREFIX/LEN IPV6_PREFIX/LEN]
+
+The encapsulation can be carried directly by a route or by the normal multipath
+and nexthop-object forms supported by LWT.
+
+The ``dev`` parameter belongs to the original-family selecting route. After
+translation, a new lookup in the translated family chooses the next hop and
+egress interface. The selected output may therefore use a different interface.
+
+CLAT control plane
+==================
+
+ipxlat only performs packet translation. A network manager must discover or
+configure the NAT64 prefix, choose the CLAT addresses, and install and remove
+the corresponding addresses, rules, and routes. The NAT64 prefix is commonly
+learned from the PREF64 option in an IPv6 Router Advertisement as specified by
+RFC 8781. RFC 7050 DNS discovery is another possible source.
+
+Each IPv6-only interface needs its own CLAT configuration and IPv4 routing
+policy. Userspace must keep the chosen NAT64 prefix, dedicated CLAT IPv6
+address, and IPv6 route on the same network attachment. It also decides whether
+to disable or deprioritize CLAT when native IPv4 connectivity becomes
+available. None of this policy is inferred by the kernel attachment.
+
+Single-host CLAT
+================
+
+The example below gives a host the CLAT IPv4 address ``192.0.0.2`` on an
+IPv6-only network. ``wan0`` is the upstream interface,
+``2001:db8:100::2`` is an additional IPv6 address used only by this CLAT
+instance, and ``64:ff9b::/96`` is the prefix used by the far-side NAT64
+service::
+
+ ip addr replace 192.0.0.2/32 dev lo
+ ip -6 addr replace 2001:db8:100::2/128 dev wan0 noprefixroute
+
+ ip rule add priority 10000 from 192.0.0.2/32 lookup 464
+ ip route replace default table 464 dev wan0 \
+ encap ipxlat rfc6052-prefix6 64:ff9b::/96 \
+ eam 192.0.0.2/32 2001:db8:100::2/128 \
+ src 192.0.0.2
+
+The CLAT IPv4 address is placed on ``lo`` because it identifies the local
+translator rather than an IPv4 neighbor on the IPv6-only link. It may instead
+be assigned as a /32 to ``wan0``. This is a deployment choice, not an ipxlat
+requirement.
+
+Within one routing domain, use a distinct /32 from the ``192.0.0.0/29`` range
+for each active CLAT. This range is reserved for IPv4 continuity mechanisms,
+and traffic sourced from it should always be sent through translation rather
+than native IPv4 routing.
+
+The IPv6 address must be distinct from addresses used by applications for
+native IPv6 connections. It is nevertheless an ordinary address from the IPv6
+network's point of view: userspace must perform Duplicate Address Detection and
+the host must answer normal Neighbor Solicitations for it. Keeping the address
+out of ordinary native IPv6 source-address selection is also a userspace
+responsibility.
+
+For an outgoing packet, the EAM translates the local source
+``192.0.0.2`` into ``2001:db8:100::2``. A remote IPv4 destination does not
+match that EAM, so it is embedded in ``64:ff9b::/96``. A reply applies the
+same rules in reverse: its source is extracted from ``64:ff9b::/96`` and its
+destination is translated by the EAM back to ``192.0.0.2``. Both mappings are
+therefore needed for this configuration.
+
+The well-known prefix ``64:ff9b::/96`` is only correct when it is also used by
+the upstream NAT64 service. A network-specific NAT64 prefix must be configured
+explicitly instead.
+
+Address assignment creates a metric-zero local-table route even with
+``noprefixroute``. That route would take precedence over a separately added
+route, so remove it before installing the translated-family local route
+carrying the same configuration::
+
+ ip -6 route del local 2001:db8:100::2/128 table local
+ ip -6 route add local 2001:db8:100::2/128 table local dev wan0 \
+ encap ipxlat rfc6052-prefix6 64:ff9b::/96 \
+ eam 192.0.0.2/32 2001:db8:100::2/128
+
+The configuration is stored independently in the outgoing IPv4 route and the
+return IPv6 local route. When the CLAT address changes, a network manager must
+update both attachments. It should first add the new address and its translated
+local route, then switch the outgoing route, and finally remove the old route
+and address. This avoids a complete connectivity gap while changing addresses.
+
+The address subsystem does not own the manually installed LWT local route. A
+network manager must therefore remove that route when removing the address. If
+translation is disabled while the IPv6 address remains assigned, it must
+replace the LWT route with an ordinary local route so native IPv6 traffic can
+still reach the host.
+
+Delegated-prefix CLAT
+=====================
+
+For IPv4 clients on ``lan0`` and an IPv6 uplink on ``wan0``, reserve
+``2001:db8:200::/96`` inside a prefix delegated to the CLAT. The EAM below
+pairs the downstream ``192.0.2.0/24`` with
+``2001:db8:200::c000:200/120``. Both prefixes have eight host bits, and the
+IPv6 form was chosen by embedding ``192.0.2.0`` in the reserved /96::
+
+ ip rule add priority 9000 iif lan0 from 192.0.2.0/24 lookup 464
+ ip route replace default table 464 dev wan0 \
+ encap ipxlat rfc6052-prefix6 64:ff9b::/96 \
+ eam 192.0.2.0/24 2001:db8:200::c000:200/120
+
+The rule sends only IPv4 packets received from the downstream network, with a
+source inside its assigned range, to the translating default route. For
+example, a packet from ``192.0.2.37`` to ``203.0.113.9`` leaves the CLAT with
+these addresses::
+
+ 2001:db8:200::c000:225 -> 64:ff9b::cb00:7109
+
+The source used the EAM while the remote destination used the far-side NAT64
+prefix. The delegated prefix must be routed to the CLAT by its upstream
+network.
+
+If downstream source validation is required, enforce it on ``lan0`` with the
+normal routing or firewall facilities. ipxlat deliberately does not decide
+which IPv4 sources are authorized.
+
+The return direction needs an IPv6 selecting route for the EAM prefix carrying
+the same configuration::
+
+ ip -6 route replace 2001:db8:200::c000:200/120 dev lan0 \
+ encap ipxlat rfc6052-prefix6 64:ff9b::/96 \
+ eam 192.0.2.0/24 2001:db8:200::c000:200/120
+
+A reply maps the two endpoints in reverse. The new IPv4 route lookup then
+sends the packet to ``192.0.2.37`` through ``lan0``.
+
+When DHCPv6-PD renumbers the delegated prefix, userspace must update the EAM
+and both selecting routes. The make-before-break ordering described for the
+single-host case also applies here.
+
+Stateful NAT64 and 464XLAT
+==========================
+
+ipxlat can provide the stateless part of a PLAT while ordinary IPv4 conntrack
+and NAT provide the stateful part. In this example ``lan6`` connects an IPv6
+client network, ``wan4`` has normal IPv4 Internet connectivity,
+``64:ff9b::/96`` is the advertised NAT64 prefix, and the EAM gives clients in
+``2001:db8:300::/120`` intermediate IPv4 identities from ``192.0.2.0/24``::
+
+ sysctl -w net.ipv4.ip_forward=1
+ sysctl -w net.ipv6.conf.all.forwarding=1
+
+ ip -6 route replace 64:ff9b::/96 dev wan4 \
+ encap ipxlat rfc6052-prefix6 64:ff9b::/96 \
+ eam 192.0.2.0/24 2001:db8:300::/120
+
+ ip route replace 192.0.2.0/24 dev lan6 \
+ encap ipxlat rfc6052-prefix6 64:ff9b::/96 \
+ eam 192.0.2.0/24 2001:db8:300::/120
+
+After 6-to-4 translation, the packet enters the normal IPv4 forwarding path.
+The intermediate IPv4 source can then be translated to the public address on
+``wan4``::
+
+ nft -f - <<'EOF'
+ table ip ipxlat_nat64 {
+ chain postrouting {
+ type nat hook postrouting priority srcnat; policy accept;
+ oifname "wan4" ip saddr 192.0.2.0/24 masquerade
+ }
+ }
+ EOF
+
+IPv4 conntrack reverses the NAT on replies before routing. The resulting
+destination in ``192.0.2.0/24`` selects the return ipxlat route, and the new
+IPv6 route lookup sends the translated packet to ``lan6``. No conntrack entry
+is carried across either family change.
+
+Putting one of the CLAT configurations above in front of this PLAT forms a
+complete 464XLAT path. The CLAT's near-side IPv6 mapping must fall within an
+IPv6 EAM configured by the PLAT.
+
+SIIT border router
+==================
+
+Pure stateless SIIT needs one selecting route in each direction: the IPv6
+mapping prefix for 6-to-4 and the corresponding IPv4 range for 4-to-6. The
+routes carry the translation configuration, while the translated-family route
+lookups choose the next hops. Conntrack and NAT are not required.
+
+Packet handling
+===============
+
+Translation itself does not decrement TTL or Hop Limit. Forwarded packets lose
+one hop in the normal translated-family forwarding path. Locally originated
+or locally terminated traffic is not forwarded, so translation does not
+decrement its TTL or Hop Limit.
+
+No conntrack object crosses the family switch. Metadata associated with the
+original family is discarded before the packet enters translated-family
+hooks. Ordinary translated-side conntrack and NAT can then be used, for
+example on the IPv4 side of a NAT64 sandwich.
+
+IPv4 packets with DF clear are translated first and may then be fragmented by
+IPv6 output using the translated route PMTU. IPv4 packets which were already
+fragments can be fragmented again while preserving their translated fragment
+identity. DF packets cannot be fragmented and use normal Packet Too Big
+feedback.
+
+If Packet Too Big feedback is lost on a known path, configure an MTU metric on
+the translated-family route. It acts as an administrative ceiling while still
+allowing PMTU discovery to learn a smaller value::
+
+ ip -6 route replace 64:ff9b::/96 via 2001:db8::1 dev wan0 mtu 1280
+
+A CLAT can also advertise a smaller IPv4 MTU to downstream clients, for
+example with the DHCP interface-MTU option. TCP MSS clamping is another
+possible operational workaround.
+
+IPv4 permits a UDP checksum of zero while ordinary IPv6 UDP does not. ipxlat
+accepts an unfragmented zero-checksum IPv4 UDP datagram and computes the
+checksum required by IPv6. An ICMP, ICMPv6, or zero-checksum UDPv4 datagram
+must be reassembled before translation if it is fragmented because generating
+the translated checksum requires the complete datagram.
+
+Protocol handling
+=================
+
+TCP, UDP, and ICMP receive the protocol-specific checksum handling required by
+translation. Other upper-layer protocol numbers are preserved and their
+payload is left unchanged. Such protocols work only when their own semantics
+remain valid across the family and address change. A checksum-neutral address
+mapping can help protocols that use an IP pseudo-header but are not understood
+by ipxlat. An AH-protected packet cannot remain valid across translation
+because AH authenticates IP-header fields changed by ipxlat.
+
+IPv4 options are not copied to IPv6. An active IPv4 source route is rejected.
+IPv6 Hop-by-Hop Options, Destination Options, and inactive Routing Headers are
+skipped rather than represented in IPv4, while an active Routing Header is
+rejected. The IPv6 Fragment Header is translated; an extension header after a
+Fragment Header is not supported.
diff --git a/net/ipxlat/Kconfig b/net/ipxlat/Kconfig
index b73a443eef0f..16c9e334e678 100644
--- a/net/ipxlat/Kconfig
+++ b/net/ipxlat/Kconfig
@@ -11,6 +11,8 @@ config NET_IPXLAT
RFC 7915. Useful for IPv6 focused networks. Particularly NAT64,
SIIT-DC, 464XLAT network architectures.
+ See also <file:Documentation/networking/ipxlat.rst>.
+
To compile this engine as a module, choose M here.
config NET_IPXLAT_LWT
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread