From: Ralf Lici <ralf@mandelbit.com>
To: netdev@vger.kernel.org
Cc: "David Ahern" <dsahern@kernel.org>,
"Ido Schimmel" <idosch@nvidia.com>,
"David S. Miller" <davem@davemloft.net>,
"Eric Dumazet" <edumazet@kernel.org>,
"Jakub Kicinski" <kuba@kernel.org>,
"Paolo Abeni" <pabeni@redhat.com>,
"Simon Horman" <horms@kernel.org>,
"Daniel Gröber" <dxld@darkboxed.org>,
"Antonio Quartulli" <antonio@mandelbit.com>,
"Toke Høiland-Jørgensen" <toke@kernel.org>,
"Beniamino Galvani" <bgalvani@redhat.com>,
linux-kernel@vger.kernel.org
Subject: [RFC net-next v2 16/18] net: ipxlat: add lightweight tunnel attachment
Date: Sat, 24 Oct 2026 11:25:26 +0200 [thread overview]
Message-ID: <20261024092532.172005-17-ralf@mandelbit.com> (raw)
In-Reply-To: <20261024092532.172005-1-ralf@mandelbit.com>
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
next prev parent reply other threads:[~2026-10-24 9:26 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` Ralf Lici [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261024092532.172005-17-ralf@mandelbit.com \
--to=ralf@mandelbit.com \
--cc=antonio@mandelbit.com \
--cc=bgalvani@redhat.com \
--cc=davem@davemloft.net \
--cc=dsahern@kernel.org \
--cc=dxld@darkboxed.org \
--cc=edumazet@kernel.org \
--cc=horms@kernel.org \
--cc=idosch@nvidia.com \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=toke@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is 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®