* [RFC net-next v2 00/18] net: introduce ipxlat stateless IPv4/IPv6 translation
@ 2026-10-24 9:25 Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 01/18] lwtunnel: track callback dispatch separately from return value Ralf Lici
` (17 more replies)
0 siblings, 18 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,
Toke Høiland-Jørgensen, Beniamino Galvani,
Xavier Hsinyuan, Daniel Gröber, Antonio Quartulli,
Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
netfilter-devel, coreteam, linux-kernel
Hi all,
apologies for the long gap since RFC v1; the attachment redesign and the
related fragmentation work took longer than expected.
This RFC introduces ipxlat, an in-kernel engine for stateless IPv4/IPv6
and ICMP translation, together with a lightweight-tunnel attachment.
ipxlat implements the SIIT algorithm from RFC 7915, RFC 6052 address
embedding, optional RFC 7757 Explicit Address Mapping, transport
checksum and GSO metadata, and ICMP translation including quoted packets
and RFC 4884 extensions. It is intended as a common building block for
CLAT, stateful NAT64, 464XLAT, and SIIT border translators.
NetworkManager currently provides CLAT with BPF [1], and
systemd-networkd is developing a similar implementation [2]. Those
implementations cannot cover some paths which need facilities outside
the BPF program, including fragmentation, fragmented ICMP, and complete
checksum generation for zero-checksum UDPv4. A reusable native
translator lets network managers share those mechanisms rather than each
maintaining another partial implementation.
This RFC has 18 patches. The first four are networking-core
prerequisites for LWT continuation and IPv6 refragmentation. Keeping the
remaining 14 changes separate makes the engine, LWT attachment,
selftests, and documentation substantially easier to review. NIPA’s
series_format will complain that the series is too long, but the split
can be adjusted for a non-RFC posting once the design has settled.
See Documentation/networking/ipxlat.rst for the full configuration and
operational model.
The latest code can also be found at:
https://codeberg.org/IPv6-Monostack/ipxlat-net-next
== Architecture ==
RFC v1 exposed ipxlat as a virtual netdevice which translated packets in
ndo_start_xmit and injected them back into the receive path. Review
identified that this required artificial device, MTU, routing, and
statistics semantics and made the packet appear to have arrived from a
device after translation.
v2 instead separates a reusable engine under net/ipxlat from its (first)
attachment. An IPv4 or IPv6 route carrying encap ipxlat selects the
packet and supplies immutable translation configuration. The selecting
route terminates in translation; after the family switch, the packet
gets a fresh route in the translated family. There is no translator
device or ipxlat-specific netlink family.
The engine performs validation and translation but does not decide which
traffic should use it. Routing rules and filters remain responsible for
deployment policy such as CLAT source selection and downstream source
validation. Keeping the engine independent also leaves room for later
netfilter or BPF kfunc attachments without duplicating the translator.
== Address configuration ==
Each attachment has one required RFC 6052 prefix and an optional RFC
7757 Explicit Address Mapping. Each source and destination is considered
independently: an EAM match takes precedence, while an address outside
the EAM uses RFC 6052 conversion.
This represents both CLAT addressing models requested during v1 review.
A single-host CLAT maps one dedicated IPv4 address to one IPv6 address.
A delegated-prefix CLAT maps the downstream IPv4 prefix into a prefix
delegated to the CLAT, while remote IPv4 destinations continue to use
the far-side NAT64 prefix. This v2 accepts at most one EAM per
attachment and does not implement RFC 7757 hairpinning.
Configuration belongs to each selecting route rather than to a shared
ipxlat object. A bidirectional setup therefore repeats the configuration
on its original and translated family selecting routes. Userspace must
update both routes whenever a configured address or prefix changes, for
example after DHCPv6-PD assigns a new prefix.
The dev associated with an ipxlat route belongs only to the
original family selecting route; it is not a translator interface and
does not select the translated packet's egress device. The fresh
translated family route lookup does that.
== Family switch and policy traversal ==
The original family route lookup ends at the LWT callback. The callback
discards route, conntrack, security, socket, and other metadata which no
longer describes the translated packet.
For received traffic, the translated packet enters the translated
family's PRE_ROUTING hook and receives a fresh route lookup. For locally
originated traffic, it enters translated family LOCAL_OUT with no
socket. The normal translated family path then owns:
- forwarding,
- TTL or Hop Limit,
- PMTU,
- XFRM,
- conntrack, and
- NAT.
This allows a stateful NAT64 PLAT to be built from stateless ipxlat plus
ordinary IPv4 conntrack and masquerading. No conntrack object crosses
the family switch.
Translated output is socketless. It uses the normal kernel-originated
UID and global XFRM policy; socket, cgroup, and per socket XFRM identity
cannot cross the family switch.
If preparation determines that a packet is valid but unsupported, the
LWT invokes the route's original input or output callback and leaves the
packet in its original family. This is needed, for example, so native
IPv6 Neighbor Discovery still works for a local address selected by an
ipxlat route. Malformed packets and failures after translation begins
are dropped.
This continuation is the central semantic choice in v2. Does PRE_ROUTING
for received translated traffic and LOCAL_OUT for locally generated
translated traffic provide the right policy visibility for a
cross-family LWT action? And does the harness discard and retain the
right skb metadata across the family switch?
== Fragmentation ==
v1 fragmented oversized DF-clear IPv4 packets before translation, using
a synthetic route to supply an MTU. v2 translates first and lets IPv6
output use the real translated route and PMTU.
IPv4 packets which are already fragments carry an IPv6 Fragment Header
after translation. This series extends IPv6 output so it can fragment
that representation again while preserving the translated
identification, base offset, and More Fragments state. IPv6 conntrack
reassembly preserves the indication needed for the later output path to
do the same. An MTU metric on a translated family route can be used as
an administrative ceiling where Packet Too Big feedback is unreliable.
If an earlier original-family component has already reassembled a
datagram, ipxlat cannot recover its previous fragment boundaries or
identification. Fragmented ICMP and zero-checksum UDPv4 must likewise be
reassembled before translation because their translated checksums need
the complete datagram.
== Testing ==
The selftests use an iproute2-configured namespace topology and cover:
- local and forwarded translation,
- native fallback,
- zero-checksum UDPv4,
- translated family conntrack and masquerading,
- route-PMTU fragmentation,
- conntrack reassembly followed by refragmentation,
- PMTU feedback, and
- ICMP errors.
Along with this series, an iproute2 ipxlat-enabled tree is provided [3].
[1] https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.57.2-dev/src/core/bpf/clat.bpf.c
[2] https://github.com/systemd/systemd/pull/41412
[3] https://codeberg.org/IPv6-Monostack/iproute2/src/branch/ipxlat-lwt
== Points for discussion ==
- Is the translated-family continuation described above the right
contract for an LWT family switch?
- Is an LWT attachment with inline immutable route configuration the
right first harness for the reusable engine?
- Is reusing an existing IPv6 Fragment Header in the output
fragmentation path a suitable general mechanism for translated IPv4
fragments?
== Acknowledgements ==
The ipxlat translation engine is derived from Jool so it can benefit
from the project's accumulated implementation experience and
golden-packet tests. Thanks to Jool's principal author, Alberto Leiva
Popper, for maintaining that work and for writing the original joolif
prototype from which ipxlat started.
Thanks to Toke Høiland-Jørgensen, Beniamino Galvani, Xavier Hsinyuan,
and Jonathan Corbet for their RFC v1 review and deployment input.
Thanks to NLnet's NGI0 Core Fund for supporting ipxlat development.
Thanks for your review,
Ralf Lici
Mandelbit SRL
---
Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-1-ralf@mandelbit.com/
- Replaced the virtual netdevice and generic-netlink control plane with
a reusable translation engine under net/ipxlat and a route-selected
LWT attachment. Deployment policy remains outside the engine. (Toke)
- Added an optional RFC 7757 EAM before RFC 6052 conversion, covering
both single-host and delegated-prefix CLAT addressing. (Beniamino)
- Defined received and local traffic continuations into the translated
family, including fresh routing, conntrack, NAT, and XFRM processing.
Added native LWT fallback for packets the engine does not support.
- Removed the lowest-ipv6-mtu parameter and IPv4 pre-fragmentation. The
translated IPv6 route now owns PMTU and fragmentation, including
refragmentation which preserves an existing Fragment Header across
conntrack reassembly.
- Moved TTL and Hop Limit handling to the translated family forwarding
path, so translation itself does not consume a hop.
- Replaced the v1 netdevice tests and documentation with LWT coverage
and deployment examples.
Ralf Lici (18):
lwtunnel: track callback dispatch separately from return value
ipv4/ipv6: add LWT receive helpers
ipv6: allow output fragmentation to reuse a Fragment Header
netfilter: preserve IPv6 refragmentation state across reassembly
net: ipxlat: add engine skeleton and configuration model
net: ipxlat: add RFC 6052 and EAM address translation
net: ipxlat: add packet metadata and header helpers
net: ipxlat: add IPv4 packet validation
net: ipxlat: add IPv6 packet validation
net: ipxlat: add checksum and offload helpers
net: ipxlat: add IPv4/IPv6 header and transport translation
net: ipxlat: add translation preparation and dispatch
net: ipxlat: add translator-generated ICMP errors
net: ipxlat: add ICMP informational translation
net: ipxlat: add ICMP error and quoted-packet translation
net: ipxlat: add lightweight tunnel attachment
selftests: net: add ipxlat coverage
docs: networking: add ipxlat guide
Documentation/networking/index.rst | 1 +
Documentation/networking/ipxlat.rst | 333 ++++++++++
include/linux/ipv6.h | 2 +
include/net/ip.h | 1 +
include/net/ipv6.h | 2 +
include/net/ipv6_frag.h | 2 +
include/net/ipxlat.h | 113 ++++
include/uapi/linux/ipxlat.h | 56 ++
include/uapi/linux/lwtunnel.h | 1 +
net/Kconfig | 1 +
net/Makefile | 1 +
net/core/lwtunnel.c | 15 +-
net/ipv4/ip_input.c | 10 +
net/ipv6/ip6_input.c | 11 +
net/ipv6/ip6_output.c | 53 +-
net/ipv6/netfilter/nf_conntrack_reasm.c | 11 +-
net/ipxlat/Kconfig | 27 +
net/ipxlat/Makefile | 19 +
net/ipxlat/address.c | 218 +++++++
net/ipxlat/address.h | 57 ++
net/ipxlat/config.c | 82 +++
net/ipxlat/core.c | 156 +++++
net/ipxlat/icmp.h | 70 ++
net/ipxlat/icmp_46.c | 588 +++++++++++++++++
net/ipxlat/icmp_64.c | 615 ++++++++++++++++++
net/ipxlat/lwt.c | 643 +++++++++++++++++++
net/ipxlat/packet.h | 243 +++++++
net/ipxlat/packet4.c | 410 ++++++++++++
net/ipxlat/packet6.c | 468 ++++++++++++++
net/ipxlat/translate_46.c | 217 +++++++
net/ipxlat/translate_46.h | 77 +++
net/ipxlat/translate_64.c | 226 +++++++
net/ipxlat/translate_64.h | 96 +++
net/ipxlat/transport.c | 179 ++++++
net/ipxlat/transport.h | 127 ++++
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 ++++++++++++++++
39 files changed, 5694 insertions(+), 10 deletions(-)
create mode 100644 Documentation/networking/ipxlat.rst
create mode 100644 include/net/ipxlat.h
create mode 100644 include/uapi/linux/ipxlat.h
create mode 100644 net/ipxlat/Kconfig
create mode 100644 net/ipxlat/Makefile
create mode 100644 net/ipxlat/address.c
create mode 100644 net/ipxlat/address.h
create mode 100644 net/ipxlat/config.c
create mode 100644 net/ipxlat/core.c
create mode 100644 net/ipxlat/icmp.h
create mode 100644 net/ipxlat/icmp_46.c
create mode 100644 net/ipxlat/icmp_64.c
create mode 100644 net/ipxlat/lwt.c
create mode 100644 net/ipxlat/packet.h
create mode 100644 net/ipxlat/packet4.c
create mode 100644 net/ipxlat/packet6.c
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
create mode 100644 net/ipxlat/transport.c
create mode 100644 net/ipxlat/transport.h
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
--
2.55.0
^ permalink raw reply [flat|nested] 19+ messages in thread
* [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
end of thread, other threads:[~2026-10-24 9:26 UTC | newest]
Thread overview: 19+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [RFC net-next v2 03/18] ipv6: allow output fragmentation to reuse a Fragment Header Ralf Lici
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 ` [RFC net-next v2 05/18] net: ipxlat: add engine skeleton and configuration model Ralf Lici
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 ` [RFC net-next v2 07/18] net: ipxlat: add packet metadata and header helpers Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 08/18] net: ipxlat: add IPv4 packet validation Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 09/18] net: ipxlat: add IPv6 " Ralf Lici
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 ` [RFC net-next v2 11/18] net: ipxlat: add IPv4/IPv6 header and transport translation Ralf Lici
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 ` [RFC net-next v2 13/18] net: ipxlat: add translator-generated ICMP errors Ralf Lici
2026-10-24 9:25 ` [RFC net-next v2 14/18] net: ipxlat: add ICMP informational translation Ralf Lici
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 ` [RFC net-next v2 16/18] net: ipxlat: add lightweight tunnel attachment 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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®