* [PATCH nf-next v4 1/6] net: netfilter: Add ether_type to net_device_path_ctx
2026-07-03 14:10 [PATCH nf-next v4 0/6] Add IPv4 over IPv6 and SIT flowtable SW acceleration Lorenzo Bianconi
@ 2026-07-03 14:10 ` Lorenzo Bianconi
2026-07-03 14:10 ` [PATCH nf-next v4 2/6] net: netfilter: Add encap_proto to flow_offload_tunnel Lorenzo Bianconi
` (4 subsequent siblings)
5 siblings, 0 replies; 11+ messages in thread
From: Lorenzo Bianconi @ 2026-07-03 14:10 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Felix Fietkau, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, David Ahern,
Ido Schimmel, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
Shuah Khan, Lorenzo Bianconi
Cc: linux-arm-kernel, linux-mediatek, netdev, netfilter-devel,
coreteam, linux-kselftest
Add an ether_type field to struct net_device_path_ctx to allow IPv6
tunnel drivers to select the appropriate L3 protocol based on the
encapsulated traffic.
Update the airoha and mtk Ethernet drivers to use the new
dev_fill_forward_path() signature.
This is a preliminary patch to enable sw flowtable acceleration for
IPv4 over IPv6 tunnels.
Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
---
drivers/net/ethernet/airoha/airoha_ppe.c | 13 ++++++++-----
drivers/net/ethernet/mediatek/mtk_ppe_offload.c | 13 ++++++++-----
include/linux/netdevice.h | 4 +++-
net/core/dev.c | 6 ++++--
net/ipv6/ip6_tunnel.c | 5 ++++-
net/netfilter/nf_flow_table_path.c | 8 +++++---
6 files changed, 32 insertions(+), 17 deletions(-)
diff --git a/drivers/net/ethernet/airoha/airoha_ppe.c b/drivers/net/ethernet/airoha/airoha_ppe.c
index e7c78293002a..06d128c67c8c 100644
--- a/drivers/net/ethernet/airoha/airoha_ppe.c
+++ b/drivers/net/ethernet/airoha/airoha_ppe.c
@@ -276,7 +276,8 @@ static int airoha_ppe_flow_mangle_ipv4(const struct flow_action_entry *act,
return 0;
}
-static int airoha_ppe_get_wdma_info(struct net_device *dev, const u8 *addr,
+static int airoha_ppe_get_wdma_info(struct net_device *dev,
+ const u8 *addr, __be16 ether_type,
struct airoha_wdma_info *info)
{
struct net_device_path_stack stack;
@@ -287,7 +288,7 @@ static int airoha_ppe_get_wdma_info(struct net_device *dev, const u8 *addr,
return -ENODEV;
rcu_read_lock();
- err = dev_fill_forward_path(dev, addr, &stack);
+ err = dev_fill_forward_path(dev, addr, ether_type, &stack);
rcu_read_unlock();
if (err)
return err;
@@ -331,7 +332,7 @@ static int airoha_ppe_foe_entry_prepare(struct airoha_eth *eth,
struct airoha_foe_entry *hwe,
struct net_device *netdev, int type,
struct airoha_flow_data *data,
- int l4proto)
+ __be16 ether_type, int l4proto)
{
u32 qdata = FIELD_PREP(AIROHA_FOE_SHAPER_ID, 0x7f), ports_pad, val;
int wlan_etype = -EINVAL, dsa_port = airoha_get_dsa_port(&netdev);
@@ -354,7 +355,7 @@ static int airoha_ppe_foe_entry_prepare(struct airoha_eth *eth,
struct airoha_wdma_info info = {};
if (!airoha_ppe_get_wdma_info(netdev, data->eth.h_dest,
- &info)) {
+ ether_type, &info)) {
val |= FIELD_PREP(AIROHA_FOE_IB2_NBQ, info.idx) |
FIELD_PREP(AIROHA_FOE_IB2_PSE_PORT,
FE_PSE_PORT_CDM4);
@@ -1081,6 +1082,7 @@ static int airoha_ppe_flow_offload_replace(struct airoha_eth *eth,
struct flow_action_entry *act;
struct airoha_foe_entry hwe;
int err, i, offload_type;
+ __be16 ether_type = 0;
u16 addr_type = 0;
u8 l4proto = 0;
@@ -1107,6 +1109,7 @@ static int airoha_ppe_flow_offload_replace(struct airoha_eth *eth,
struct flow_match_basic match;
flow_rule_match_basic(rule, &match);
+ ether_type = match.key->n_proto;
l4proto = match.key->ip_proto;
} else {
return -EOPNOTSUPP;
@@ -1177,7 +1180,7 @@ static int airoha_ppe_flow_offload_replace(struct airoha_eth *eth,
return -EINVAL;
err = airoha_ppe_foe_entry_prepare(eth, &hwe, odev, offload_type,
- &data, l4proto);
+ &data, ether_type, l4proto);
if (err)
return err;
diff --git a/drivers/net/ethernet/mediatek/mtk_ppe_offload.c b/drivers/net/ethernet/mediatek/mtk_ppe_offload.c
index cc8c4ef8038f..2601c17b29c8 100644
--- a/drivers/net/ethernet/mediatek/mtk_ppe_offload.c
+++ b/drivers/net/ethernet/mediatek/mtk_ppe_offload.c
@@ -89,7 +89,8 @@ mtk_flow_offload_mangle_eth(const struct flow_action_entry *act, void *eth)
}
static int
-mtk_flow_get_wdma_info(struct net_device *dev, const u8 *addr, struct mtk_wdma_info *info)
+mtk_flow_get_wdma_info(struct net_device *dev, const u8 *addr,
+ __be16 ether_type, struct mtk_wdma_info *info)
{
struct net_device_path_stack stack;
struct net_device_path *path;
@@ -102,7 +103,7 @@ mtk_flow_get_wdma_info(struct net_device *dev, const u8 *addr, struct mtk_wdma_i
return -1;
rcu_read_lock();
- err = dev_fill_forward_path(dev, addr, &stack);
+ err = dev_fill_forward_path(dev, addr, ether_type, &stack);
rcu_read_unlock();
if (err)
return err;
@@ -190,12 +191,12 @@ mtk_flow_get_dsa_port(struct net_device **dev)
static int
mtk_flow_set_output_device(struct mtk_eth *eth, struct mtk_foe_entry *foe,
struct net_device *dev, const u8 *dest_mac,
- int *wed_index)
+ __be16 ether_type, int *wed_index)
{
struct mtk_wdma_info info = {};
int pse_port, dsa_port, queue;
- if (mtk_flow_get_wdma_info(dev, dest_mac, &info) == 0) {
+ if (mtk_flow_get_wdma_info(dev, dest_mac, ether_type, &info) == 0) {
mtk_foe_entry_set_wdma(eth, foe, info.wdma_idx, info.queue,
info.bss, info.wcid, info.amsdu);
if (mtk_is_netsys_v2_or_greater(eth)) {
@@ -273,6 +274,7 @@ mtk_flow_offload_replace(struct mtk_eth *eth, struct flow_cls_offload *f,
struct mtk_flow_data data = {};
struct mtk_foe_entry foe;
struct mtk_flow_entry *entry;
+ __be16 ether_type = 0;
int offload_type = 0;
int wed_index = -1;
u16 addr_type = 0;
@@ -319,6 +321,7 @@ mtk_flow_offload_replace(struct mtk_eth *eth, struct flow_cls_offload *f,
struct flow_match_basic match;
flow_rule_match_basic(rule, &match);
+ ether_type = match.key->n_proto;
l4proto = match.key->ip_proto;
} else {
return -EOPNOTSUPP;
@@ -481,7 +484,7 @@ mtk_flow_offload_replace(struct mtk_eth *eth, struct flow_cls_offload *f,
mtk_foe_entry_set_pppoe(eth, &foe, data.pppoe.sid);
err = mtk_flow_set_output_device(eth, &foe, odev, data.eth.h_dest,
- &wed_index);
+ ether_type, &wed_index);
if (err)
return err;
diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 9981d637f8b5..a97aee0e49b2 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -938,6 +938,7 @@ struct net_device_path_stack {
struct net_device_path_ctx {
const struct net_device *dev;
u8 daddr[ETH_ALEN];
+ __be16 ether_type;
int num_vlans;
struct {
@@ -3420,7 +3421,8 @@ void dev_remove_offload(struct packet_offload *po);
int dev_get_iflink(const struct net_device *dev);
int dev_fill_metadata_dst(struct net_device *dev, struct sk_buff *skb);
-int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr,
+int dev_fill_forward_path(const struct net_device *dev,
+ const u8 *daddr, __be16 ether_type,
struct net_device_path_stack *stack);
struct net_device *dev_get_by_name(struct net *net, const char *name);
struct net_device *dev_get_by_name_rcu(struct net *net, const char *name);
diff --git a/net/core/dev.c b/net/core/dev.c
index 4b3d5cfdf6e0..9f5a2da9e9f7 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -750,12 +750,14 @@ static struct net_device_path *dev_fwd_path(struct net_device_path_stack *stack)
return &stack->path[k];
}
-int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr,
+int dev_fill_forward_path(const struct net_device *dev,
+ const u8 *daddr, __be16 ether_type,
struct net_device_path_stack *stack)
{
const struct net_device *last_dev;
struct net_device_path_ctx ctx = {
- .dev = dev,
+ .dev = dev,
+ .ether_type = ether_type,
};
struct net_device_path *path;
int ret = 0;
diff --git a/net/ipv6/ip6_tunnel.c b/net/ipv6/ip6_tunnel.c
index bf8e40af60b0..38da07101601 100644
--- a/net/ipv6/ip6_tunnel.c
+++ b/net/ipv6/ip6_tunnel.c
@@ -1863,7 +1863,10 @@ static int ip6_tnl_fill_forward_path(struct net_device_path_ctx *ctx,
path->type = DEV_PATH_TUN;
path->tun.src_v6 = t->parms.laddr;
path->tun.dst_v6 = t->parms.raddr;
- path->tun.l3_proto = IPPROTO_IPV6;
+ if (ctx->ether_type == cpu_to_be16(ETH_P_IP))
+ path->tun.l3_proto = IPPROTO_IPIP;
+ else
+ path->tun.l3_proto = IPPROTO_IPV6;
path->dev = ctx->dev;
ctx->dev = dst->dev;
}
diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
index 98c03b487f52..c8011ec36532 100644
--- a/net/netfilter/nf_flow_table_path.c
+++ b/net/netfilter/nf_flow_table_path.c
@@ -45,7 +45,8 @@ static bool nft_is_valid_ether_device(const struct net_device *dev)
static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
const struct dst_entry *dst_cache,
const struct nf_conn *ct,
- enum ip_conntrack_dir dir, u8 *ha,
+ enum ip_conntrack_dir dir,
+ u8 *ha, __be16 ether_type,
struct net_device_path_stack *stack)
{
const void *daddr = &ct->tuplehash[!dir].tuple.src.u3;
@@ -72,7 +73,7 @@ static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
return -1;
out:
- return dev_fill_forward_path(dev, ha, stack);
+ return dev_fill_forward_path(dev, ha, ether_type, stack);
}
struct nft_forward_info {
@@ -255,7 +256,8 @@ static int nft_dev_forward_path(const struct nft_pktinfo *pkt,
unsigned char ha[ETH_ALEN];
int i;
- if (nft_dev_fill_forward_path(route, dst, ct, dir, ha, &stack) < 0 ||
+ if (nft_dev_fill_forward_path(route, dst, ct, dir, ha, pkt->ethertype,
+ &stack) < 0 ||
nft_dev_path_info(&stack, &info, ha, &ft->data) < 0)
return -ENOENT;
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH nf-next v4 2/6] net: netfilter: Add encap_proto to flow_offload_tunnel
2026-07-03 14:10 [PATCH nf-next v4 0/6] Add IPv4 over IPv6 and SIT flowtable SW acceleration Lorenzo Bianconi
2026-07-03 14:10 ` [PATCH nf-next v4 1/6] net: netfilter: Add ether_type to net_device_path_ctx Lorenzo Bianconi
@ 2026-07-03 14:10 ` Lorenzo Bianconi
2026-07-03 14:10 ` [PATCH nf-next v4 3/6] net: netfilter: Add IPv4 over IPv6 tunnel flowtable acceleration Lorenzo Bianconi
` (3 subsequent siblings)
5 siblings, 0 replies; 11+ messages in thread
From: Lorenzo Bianconi @ 2026-07-03 14:10 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Felix Fietkau, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, David Ahern,
Ido Schimmel, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
Shuah Khan, Lorenzo Bianconi
Cc: linux-arm-kernel, linux-mediatek, netdev, netfilter-devel,
coreteam, linux-kselftest
Add encap_proto (AF_INET or AF_INET6) to struct flow_offload_tunnel
to allow its use as part of the hash table key during flowtable entry
lookup.
This is a preliminary change to support IPv4 over IPv6 tunneling via
the flowtable infrastructure for software acceleration.
Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
---
include/linux/netdevice.h | 1 +
include/net/netfilter/nf_flow_table.h | 1 +
net/ipv4/ipip.c | 1 +
net/ipv6/ip6_tunnel.c | 1 +
net/netfilter/nf_flow_table_ip.c | 2 ++
net/netfilter/nf_flow_table_path.c | 2 ++
6 files changed, 8 insertions(+)
diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index a97aee0e49b2..45d99e11b06e 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -902,6 +902,7 @@ struct net_device_path {
};
u8 l3_proto;
+ u8 encap_proto;
} tun;
struct {
enum {
diff --git a/include/net/netfilter/nf_flow_table.h b/include/net/netfilter/nf_flow_table.h
index 7b23b245a5a8..4d406801ec90 100644
--- a/include/net/netfilter/nf_flow_table.h
+++ b/include/net/netfilter/nf_flow_table.h
@@ -118,6 +118,7 @@ struct flow_offload_tunnel {
};
u8 l3_proto;
+ u8 encap_proto;
};
struct flow_offload_tuple {
diff --git a/net/ipv4/ipip.c b/net/ipv4/ipip.c
index b643194f57d2..9f7b2bdabef0 100644
--- a/net/ipv4/ipip.c
+++ b/net/ipv4/ipip.c
@@ -369,6 +369,7 @@ static int ipip_fill_forward_path(struct net_device_path_ctx *ctx,
path->tun.src_v4.s_addr = tiph->saddr;
path->tun.dst_v4.s_addr = tiph->daddr;
path->tun.l3_proto = IPPROTO_IPIP;
+ path->tun.encap_proto = AF_INET;
path->dev = ctx->dev;
ctx->dev = rt->dst.dev;
diff --git a/net/ipv6/ip6_tunnel.c b/net/ipv6/ip6_tunnel.c
index 38da07101601..a121f715afd2 100644
--- a/net/ipv6/ip6_tunnel.c
+++ b/net/ipv6/ip6_tunnel.c
@@ -1863,6 +1863,7 @@ static int ip6_tnl_fill_forward_path(struct net_device_path_ctx *ctx,
path->type = DEV_PATH_TUN;
path->tun.src_v6 = t->parms.laddr;
path->tun.dst_v6 = t->parms.raddr;
+ path->tun.encap_proto = AF_INET6;
if (ctx->ether_type == cpu_to_be16(ETH_P_IP))
path->tun.l3_proto = IPPROTO_IPIP;
else
diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c
index 29e93ac1e2e4..cf2c74e3fd56 100644
--- a/net/netfilter/nf_flow_table_ip.c
+++ b/net/netfilter/nf_flow_table_ip.c
@@ -198,6 +198,7 @@ static void nf_flow_tuple_encap(struct nf_flowtable_ctx *ctx,
tuple->tun.dst_v4.s_addr = iph->daddr;
tuple->tun.src_v4.s_addr = iph->saddr;
tuple->tun.l3_proto = IPPROTO_IPIP;
+ tuple->tun.encap_proto = AF_INET;
}
break;
case htons(ETH_P_IPV6):
@@ -206,6 +207,7 @@ static void nf_flow_tuple_encap(struct nf_flowtable_ctx *ctx,
tuple->tun.dst_v6 = ip6h->daddr;
tuple->tun.src_v6 = ip6h->saddr;
tuple->tun.l3_proto = IPPROTO_IPV6;
+ tuple->tun.encap_proto = AF_INET6;
}
break;
default:
diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
index c8011ec36532..caaf48c5fd2a 100644
--- a/net/netfilter/nf_flow_table_path.c
+++ b/net/netfilter/nf_flow_table_path.c
@@ -129,6 +129,7 @@ static int nft_dev_path_info(const struct net_device_path_stack *stack,
info->tun.src_v6 = path->tun.src_v6;
info->tun.dst_v6 = path->tun.dst_v6;
info->tun.l3_proto = path->tun.l3_proto;
+ info->tun.encap_proto = path->tun.encap_proto;
info->num_tuns++;
} else {
if (info->num_encaps >= NF_FLOW_TABLE_ENCAP_MAX)
@@ -278,6 +279,7 @@ static int nft_dev_forward_path(const struct nft_pktinfo *pkt,
route->tuple[!dir].in.tun.src_v6 = info.tun.dst_v6;
route->tuple[!dir].in.tun.dst_v6 = info.tun.src_v6;
route->tuple[!dir].in.tun.l3_proto = info.tun.l3_proto;
+ route->tuple[!dir].in.tun.encap_proto = info.tun.encap_proto;
route->tuple[!dir].in.num_tuns = info.num_tuns;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH nf-next v4 3/6] net: netfilter: Add IPv4 over IPv6 tunnel flowtable acceleration
2026-07-03 14:10 [PATCH nf-next v4 0/6] Add IPv4 over IPv6 and SIT flowtable SW acceleration Lorenzo Bianconi
2026-07-03 14:10 ` [PATCH nf-next v4 1/6] net: netfilter: Add ether_type to net_device_path_ctx Lorenzo Bianconi
2026-07-03 14:10 ` [PATCH nf-next v4 2/6] net: netfilter: Add encap_proto to flow_offload_tunnel Lorenzo Bianconi
@ 2026-07-03 14:10 ` Lorenzo Bianconi
2026-07-07 16:25 ` Lorenzo Bianconi
2026-07-08 10:11 ` Lorenzo Bianconi
2026-07-03 14:10 ` [PATCH nf-next v4 4/6] selftests: netfilter: nft_flowtable.sh: Add IPv4 over IPv6 flowtable selftest Lorenzo Bianconi
` (2 subsequent siblings)
5 siblings, 2 replies; 11+ messages in thread
From: Lorenzo Bianconi @ 2026-07-03 14:10 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Felix Fietkau, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, David Ahern,
Ido Schimmel, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
Shuah Khan, Lorenzo Bianconi
Cc: linux-arm-kernel, linux-mediatek, netdev, netfilter-devel,
coreteam, linux-kselftest
Introduce sw flowtable acceleration for the TX/RX paths of
IPv4 over IPv6 tunnels, relying on the netfilter flowtable
infrastructure.
The feature can be tested with a forwarding scenario between two
NICs (eth0 and eth1), where an IPv4 over IPv6 tunnel is used to
reach a remote site via eth1 as the underlay device:
ETH0 -- TUN0 <==> ETH1 -- [IP network] -- TUN1 (2001:db8:2::2)
[IP configuration]
6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 00:00:22:33:11:55 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.2/24 scope global eth0
valid_lft forever preferred_lft forever
7: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 00:11:22:33:11:55 brd ff:ff:ff:ff:ff:ff
inet6 2001:db8:2::1/64 scope global nodad
valid_lft forever preferred_lft forever
8: tun0@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
link/tunnel6 2001:db8:2::1 peer 2001:db8:2::2 permaddr ce9c:2940:7dcc::
inet 192.168.100.1/24 scope global tun0
valid_lft forever preferred_lft forever
$ ip route show
default via 192.168.100.2 dev tun0
192.168.0.0/24 dev eth0 proto kernel scope link src 192.168.0.2
192.168.100.0/24 dev tun0 proto kernel scope link src 192.168.100.1
$ ip -6 route show
2001:db8:2::/64 dev eth1 proto kernel metric 256 pref medium
$ nft list ruleset
table inet filter {
flowtable ft {
hook ingress priority filter
devices = { eth0, eth1 }
}
chain forward {
type filter hook forward priority filter; policy accept;
meta l4proto { tcp, udp } flow add @ft
}
}
When reproducing this scenario using veth interfaces, the following
results were observed:
- TCP stream received from IPv4 over IPv6 tunnel:
- net-next (baseline): ~126 Gbps
- net-next + IP6IP flowtable support: ~138 Gbps
- TCP stream transmitted to IPv4 over IPv6 tunnel:
- net-next (baseline): ~127 Gbps
- net-next + IP6IP flowtable support: ~140 Gbps
Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
---
net/netfilter/nf_flow_table_core.c | 16 +++-
net/netfilter/nf_flow_table_ip.c | 161 ++++++++++++++++++++++++++-----------
net/netfilter/nf_flow_table_path.c | 10 ++-
3 files changed, 133 insertions(+), 54 deletions(-)
diff --git a/net/netfilter/nf_flow_table_core.c b/net/netfilter/nf_flow_table_core.c
index 99c5b9d671a0..18f89e6fb435 100644
--- a/net/netfilter/nf_flow_table_core.c
+++ b/net/netfilter/nf_flow_table_core.c
@@ -76,9 +76,14 @@ struct flow_offload *flow_offload_alloc(struct nf_conn *ct)
}
EXPORT_SYMBOL_GPL(flow_offload_alloc);
-static u32 flow_offload_dst_cookie(struct flow_offload_tuple *flow_tuple)
+static u32 flow_offload_dst_cookie(struct flow_offload_tuple *flow_tuple,
+ u8 tun_encap_proto)
{
- if (flow_tuple->l3proto == NFPROTO_IPV6)
+ bool dst_v6;
+
+ dst_v6 = tun_encap_proto ? tun_encap_proto == NFPROTO_IPV6
+ : flow_tuple->l3proto == NFPROTO_IPV6;
+ if (dst_v6)
return rt6_get_cookie(dst_rt6_info(flow_tuple->dst_cache));
return 0;
@@ -99,10 +104,12 @@ static int flow_offload_fill_route(struct flow_offload *flow,
enum flow_offload_tuple_dir dir)
{
struct flow_offload_tuple *flow_tuple = &flow->tuplehash[dir].tuple;
+ u8 l3proto, encap_proto = route->tuple[!dir].in.tun.encap_proto;
struct dst_entry *dst = nft_route_dst_fetch(route, dir);
int i, j = 0;
- switch (flow_tuple->l3proto) {
+ l3proto = encap_proto ? encap_proto : flow_tuple->l3proto;
+ switch (l3proto) {
case NFPROTO_IPV4:
flow_tuple->mtu = ip_dst_mtu_maybe_forward(dst, true);
break;
@@ -138,7 +145,8 @@ static int flow_offload_fill_route(struct flow_offload *flow,
case FLOW_OFFLOAD_XMIT_NEIGH:
flow_tuple->ifidx = route->tuple[dir].out.ifindex;
flow_tuple->dst_cache = dst;
- flow_tuple->dst_cookie = flow_offload_dst_cookie(flow_tuple);
+ flow_tuple->dst_cookie = flow_offload_dst_cookie(flow_tuple,
+ encap_proto);
break;
default:
WARN_ON_ONCE(1);
diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c
index cf2c74e3fd56..4b6de16bd4f3 100644
--- a/net/netfilter/nf_flow_table_ip.c
+++ b/net/netfilter/nf_flow_table_ip.c
@@ -191,27 +191,27 @@ static void nf_flow_tuple_encap(struct nf_flowtable_ctx *ctx,
break;
}
- switch (inner_proto) {
- case htons(ETH_P_IP):
- iph = (struct iphdr *)(skb_network_header(skb) + offset);
- if (ctx->tun.proto == IPPROTO_IPIP) {
+ if (ctx->tun.proto == IPPROTO_IPIP || ctx->tun.proto == IPPROTO_IPV6) {
+ switch (inner_proto) {
+ case htons(ETH_P_IP):
+ iph = (struct iphdr *)(skb_network_header(skb) +
+ offset);
tuple->tun.dst_v4.s_addr = iph->daddr;
tuple->tun.src_v4.s_addr = iph->saddr;
- tuple->tun.l3_proto = IPPROTO_IPIP;
+ tuple->tun.l3_proto = ctx->tun.proto;
tuple->tun.encap_proto = AF_INET;
- }
- break;
- case htons(ETH_P_IPV6):
- ip6h = (struct ipv6hdr *)(skb_network_header(skb) + offset);
- if (ctx->tun.proto == IPPROTO_IPV6) {
+ break;
+ case htons(ETH_P_IPV6):
+ ip6h = (struct ipv6hdr *)(skb_network_header(skb) +
+ offset);
tuple->tun.dst_v6 = ip6h->daddr;
tuple->tun.src_v6 = ip6h->saddr;
- tuple->tun.l3_proto = IPPROTO_IPV6;
+ tuple->tun.l3_proto = ctx->tun.proto;
tuple->tun.encap_proto = AF_INET6;
+ break;
+ default:
+ break;
}
- break;
- default:
- break;
}
}
@@ -363,7 +363,7 @@ static bool nf_flow_ip6_tunnel_proto(struct nf_flowtable_ctx *ctx,
if (ipv6_ext_hdr(ip6h->nexthdr))
return false;
- if (ip6h->nexthdr == IPPROTO_IPV6) {
+ if (ip6h->nexthdr == IPPROTO_IPIP || ip6h->nexthdr == IPPROTO_IPV6) {
ctx->tun.proto = ip6h->nexthdr;
ctx->tun.hdr_size = sizeof(*ip6h);
ctx->offset += ctx->tun.hdr_size;
@@ -384,6 +384,10 @@ static void nf_flow_ip_tunnel_pop(struct nf_flowtable_ctx *ctx,
skb_pull(skb, ctx->tun.hdr_size);
skb_reset_network_header(skb);
+ if (ctx->tun.proto == IPPROTO_IPIP)
+ skb->protocol = htons(ETH_P_IP);
+ else
+ skb->protocol = htons(ETH_P_IPV6);
}
static bool nf_flow_skb_encap_protocol(struct nf_flowtable_ctx *ctx,
@@ -489,8 +493,16 @@ static int nf_flow_offload_forward(struct nf_flowtable_ctx *ctx,
flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
mtu = flow->tuplehash[dir].tuple.mtu + ctx->offset;
- if (flow->tuplehash[!dir].tuple.tun_num)
+ switch (flow->tuplehash[!dir].tuple.tun.encap_proto) {
+ case AF_INET:
mtu -= sizeof(*iph);
+ break;
+ case AF_INET6:
+ mtu -= sizeof(struct ipv6hdr);
+ break;
+ default:
+ break;
+ }
if (unlikely(nf_flow_exceeds_mtu(skb, mtu)))
return 0;
@@ -636,56 +648,57 @@ static int nf_flow_tunnel_ipip_push(struct net *net, struct sk_buff *skb,
return 0;
}
-static int nf_flow_tunnel_v4_push(struct net *net, struct sk_buff *skb,
- struct flow_offload_tuple *tuple,
- __be32 *ip_daddr)
-{
- if (tuple->tun_num)
- return nf_flow_tunnel_ipip_push(net, skb, tuple, ip_daddr);
-
- return 0;
-}
-
static int nf_flow_tunnel_ip6ip6_push(struct net *net, struct sk_buff *skb,
struct flow_offload_tuple *tuple,
struct in6_addr **ip6_daddr)
{
- struct ipv6hdr *ip6h = (struct ipv6hdr *)skb_network_header(skb);
- struct rtable *rt = dst_rtable(tuple->dst_cache);
- __u8 dsfield = ipv6_get_dsfield(ip6h);
+ struct dst_entry *dst = tuple->dst_cache;
struct flowi6 fl6 = {
.daddr = tuple->tun.src_v6,
.saddr = tuple->tun.dst_v6,
.flowi6_proto = IPPROTO_IPV6,
};
- u8 hop_limit = ip6h->hop_limit;
+ u8 hop_limit, dsfield;
+ struct ipv6hdr *ip6h;
int err, mtu;
u32 headroom;
+ if (tuple->tun.l3_proto == IPPROTO_IPIP) {
+ struct iphdr *iph = (struct iphdr *)skb_network_header(skb);
+
+ dsfield = ipv4_get_dsfield(iph);
+ hop_limit = iph->ttl;
+ } else {
+ ip6h = (struct ipv6hdr *)skb_network_header(skb);
+ dsfield = ipv6_get_dsfield(ip6h);
+ hop_limit = ip6h->hop_limit;
+ }
+
err = iptunnel_handle_offloads(skb, SKB_GSO_IPXIP6);
if (err)
return err;
- skb_set_inner_ipproto(skb, IPPROTO_IPV6);
- headroom = sizeof(*ip6h) + LL_RESERVED_SPACE(rt->dst.dev) +
- rt->dst.header_len;
+ skb_set_inner_ipproto(skb, tuple->tun.l3_proto);
+ headroom = sizeof(*ip6h) + LL_RESERVED_SPACE(dst->dev) +
+ dst->header_len;
err = skb_cow_head(skb, headroom);
if (err)
return err;
skb_scrub_packet(skb, true);
- mtu = dst_mtu(&rt->dst) - sizeof(*ip6h);
+ mtu = dst_mtu(dst) - sizeof(*ip6h);
mtu = max(mtu, IPV6_MIN_MTU);
skb_dst_update_pmtu_no_confirm(skb, mtu);
skb_push(skb, sizeof(*ip6h));
skb_reset_network_header(skb);
+ skb->protocol = htons(ETH_P_IPV6);
ip6h = ipv6_hdr(skb);
ip6_flow_hdr(ip6h, dsfield,
ip6_make_flowlabel(net, skb, fl6.flowlabel, true, &fl6));
ip6h->hop_limit = hop_limit;
- ip6h->nexthdr = IPPROTO_IPV6;
+ ip6h->nexthdr = tuple->tun.l3_proto;
ip6h->daddr = tuple->tun.src_v6;
ip6h->saddr = tuple->tun.dst_v6;
ipv6_hdr(skb)->payload_len = htons(skb->len - sizeof(*ip6h));
@@ -696,6 +709,20 @@ static int nf_flow_tunnel_ip6ip6_push(struct net *net, struct sk_buff *skb,
return 0;
}
+static int nf_flow_tunnel_push(struct net *net, struct sk_buff *skb,
+ struct flow_offload_tuple *tuple,
+ __be32 *ip_daddr, struct in6_addr **ip6_daddr)
+{
+ switch (tuple->tun.encap_proto) {
+ case AF_INET:
+ return nf_flow_tunnel_ipip_push(net, skb, tuple, ip_daddr);
+ case AF_INET6:
+ return nf_flow_tunnel_ip6ip6_push(net, skb, tuple, ip6_daddr);
+ default:
+ return 0;
+ }
+}
+
static int nf_flow_tunnel_v6_push(struct net *net, struct sk_buff *skb,
struct flow_offload_tuple *tuple,
struct in6_addr **ip6_daddr)
@@ -815,6 +842,7 @@ nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
.in = state->in,
};
struct nf_flow_xmit xmit = {};
+ struct in6_addr *ip6_daddr;
struct flow_offload *flow;
struct neighbour *neigh;
struct rtable *rt;
@@ -844,25 +872,41 @@ nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
other_tuple = &flow->tuplehash[!dir].tuple;
ip_daddr = other_tuple->src_v4.s_addr;
- if (nf_flow_tunnel_v4_push(state->net, skb, other_tuple, &ip_daddr) < 0)
+ if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
+ &ip6_daddr) < 0)
return NF_DROP;
switch (tuplehash->tuple.xmit_type) {
- case FLOW_OFFLOAD_XMIT_NEIGH:
- rt = dst_rtable(tuplehash->tuple.dst_cache);
+ case FLOW_OFFLOAD_XMIT_NEIGH: {
+ struct dst_entry *dst;
+
xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
if (!xmit.outdev) {
flow_offload_teardown(flow);
return NF_DROP;
}
- neigh = ip_neigh_gw4(rt->dst.dev, rt_nexthop(rt, ip_daddr));
+ if (other_tuple->tun.encap_proto == AF_INET6 ||
+ ctx.tun.proto == IPPROTO_IPV6) {
+ struct rt6_info *rt6;
+
+ rt6 = dst_rt6_info(tuplehash->tuple.dst_cache);
+ neigh = ip_neigh_gw6(rt6->dst.dev,
+ rt6_nexthop(rt6, ip6_daddr));
+ dst = &rt6->dst;
+ } else {
+ rt = dst_rtable(tuplehash->tuple.dst_cache);
+ neigh = ip_neigh_gw4(rt->dst.dev,
+ rt_nexthop(rt, ip_daddr));
+ dst = &rt->dst;
+ }
if (IS_ERR(neigh)) {
flow_offload_teardown(flow);
return NF_DROP;
}
xmit.dest = neigh->ha;
- skb_dst_set_noref(skb, &rt->dst);
+ skb_dst_set_noref(skb, dst);
break;
+ }
case FLOW_OFFLOAD_XMIT_DIRECT:
xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
if (!xmit.outdev) {
@@ -1112,8 +1156,12 @@ nf_flow_offload_ipv6_lookup(struct nf_flowtable_ctx *ctx,
if (!nf_flow_skb_encap_protocol(ctx, skb, htons(ETH_P_IPV6)))
return NULL;
- if (nf_flow_tuple_ipv6(ctx, skb, &tuple) < 0)
+ if (ctx->tun.proto == IPPROTO_IPIP) {
+ if (nf_flow_tuple_ip(ctx, skb, &tuple) < 0)
+ return NULL;
+ } else if (nf_flow_tuple_ipv6(ctx, skb, &tuple) < 0) {
return NULL;
+ }
return flow_offload_lookup(flow_table, &tuple);
}
@@ -1140,7 +1188,11 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
if (tuplehash == NULL)
return NF_ACCEPT;
- ret = nf_flow_offload_ipv6_forward(&ctx, flow_table, tuplehash, skb);
+ if (ctx.tun.proto == IPPROTO_IPIP)
+ ret = nf_flow_offload_forward(&ctx, flow_table, tuplehash, skb);
+ else
+ ret = nf_flow_offload_ipv6_forward(&ctx, flow_table, tuplehash,
+ skb);
if (ret < 0)
return NF_DROP;
else if (ret == 0)
@@ -1164,21 +1216,38 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
return NF_DROP;
switch (tuplehash->tuple.xmit_type) {
- case FLOW_OFFLOAD_XMIT_NEIGH:
- rt = dst_rt6_info(tuplehash->tuple.dst_cache);
+ case FLOW_OFFLOAD_XMIT_NEIGH: {
+ struct dst_entry *dst;
+
xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
if (!xmit.outdev) {
flow_offload_teardown(flow);
return NF_DROP;
}
- neigh = ip_neigh_gw6(rt->dst.dev, rt6_nexthop(rt, ip6_daddr));
+ if (other_tuple->tun.encap_proto == AF_INET ||
+ ctx.tun.proto == IPPROTO_IPIP) {
+ __be32 ip_daddr = other_tuple->src_v4.s_addr;
+ struct rtable *rt4;
+
+ skb->protocol = htons(ETH_P_IP);
+ rt4 = dst_rtable(tuplehash->tuple.dst_cache);
+ neigh = ip_neigh_gw4(rt4->dst.dev,
+ rt_nexthop(rt4, ip_daddr));
+ dst = &rt4->dst;
+ } else {
+ rt = dst_rt6_info(tuplehash->tuple.dst_cache);
+ neigh = ip_neigh_gw6(rt->dst.dev,
+ rt6_nexthop(rt, ip6_daddr));
+ dst = &rt->dst;
+ }
if (IS_ERR(neigh)) {
flow_offload_teardown(flow);
return NF_DROP;
}
xmit.dest = neigh->ha;
- skb_dst_set_noref(skb, &rt->dst);
+ skb_dst_set_noref(skb, dst);
break;
+ }
case FLOW_OFFLOAD_XMIT_DIRECT:
xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
if (!xmit.outdev) {
diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
index caaf48c5fd2a..5e84b7f18a26 100644
--- a/net/netfilter/nf_flow_table_path.c
+++ b/net/netfilter/nf_flow_table_path.c
@@ -216,12 +216,13 @@ static int nft_flow_tunnel_update_route(const struct nft_pktinfo *pkt,
struct dst_entry *tun_dst = NULL;
struct flowi fl = {};
- switch (nft_pf(pkt)) {
+ switch (tun->encap_proto) {
case NFPROTO_IPV4:
fl.u.ip4.daddr = tun->dst_v4.s_addr;
fl.u.ip4.saddr = tun->src_v4.s_addr;
fl.u.ip4.flowi4_iif = nft_in(pkt)->ifindex;
- fl.u.ip4.flowi4_dscp = ip4h_dscp(ip_hdr(pkt->skb));
+ if (nft_pf(pkt) == NFPROTO_IPV4)
+ fl.u.ip4.flowi4_dscp = ip4h_dscp(ip_hdr(pkt->skb));
fl.u.ip4.flowi4_mark = pkt->skb->mark;
fl.u.ip4.flowi4_flags = FLOWI_FLAG_ANYSRC;
break;
@@ -229,13 +230,14 @@ static int nft_flow_tunnel_update_route(const struct nft_pktinfo *pkt,
fl.u.ip6.daddr = tun->dst_v6;
fl.u.ip6.saddr = tun->src_v6;
fl.u.ip6.flowi6_iif = nft_in(pkt)->ifindex;
- fl.u.ip6.flowlabel = ip6_flowinfo(ipv6_hdr(pkt->skb));
+ if (nft_pf(pkt) == NFPROTO_IPV6)
+ fl.u.ip6.flowlabel = ip6_flowinfo(ipv6_hdr(pkt->skb));
fl.u.ip6.flowi6_mark = pkt->skb->mark;
fl.u.ip6.flowi6_flags = FLOWI_FLAG_ANYSRC;
break;
}
- nf_route(nft_net(pkt), &tun_dst, &fl, false, nft_pf(pkt));
+ nf_route(nft_net(pkt), &tun_dst, &fl, false, tun->encap_proto);
if (!tun_dst)
return -ENOENT;
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH nf-next v4 3/6] net: netfilter: Add IPv4 over IPv6 tunnel flowtable acceleration
2026-07-03 14:10 ` [PATCH nf-next v4 3/6] net: netfilter: Add IPv4 over IPv6 tunnel flowtable acceleration Lorenzo Bianconi
@ 2026-07-07 16:25 ` Lorenzo Bianconi
2026-07-08 10:11 ` Lorenzo Bianconi
1 sibling, 0 replies; 11+ messages in thread
From: Lorenzo Bianconi @ 2026-07-07 16:25 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Felix Fietkau, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, David Ahern,
Ido Schimmel, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
Shuah Khan
Cc: linux-arm-kernel, linux-mediatek, netdev, netfilter-devel,
coreteam, linux-kselftest
[-- Attachment #1: Type: text/plain, Size: 7867 bytes --]
commenting on shashiko's report:
https://sashiko.dev/#/patchset/20260703-b4-flowtable-sw-accel-ip6ip-v4-0-00398cd12382%40kernel.org
> Introduce sw flowtable acceleration for the TX/RX paths of
> IPv4 over IPv6 tunnels, relying on the netfilter flowtable
> infrastructure.
> The feature can be tested with a forwarding scenario between two
> NICs (eth0 and eth1), where an IPv4 over IPv6 tunnel is used to
> reach a remote site via eth1 as the underlay device:
>
> ETH0 -- TUN0 <==> ETH1 -- [IP network] -- TUN1 (2001:db8:2::2)
>
> [IP configuration]
>
> 6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
> link/ether 00:00:22:33:11:55 brd ff:ff:ff:ff:ff:ff
> inet 192.168.0.2/24 scope global eth0
> valid_lft forever preferred_lft forever
> 7: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
> link/ether 00:11:22:33:11:55 brd ff:ff:ff:ff:ff:ff
> inet6 2001:db8:2::1/64 scope global nodad
> valid_lft forever preferred_lft forever
> 8: tun0@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
> link/tunnel6 2001:db8:2::1 peer 2001:db8:2::2 permaddr ce9c:2940:7dcc::
> inet 192.168.100.1/24 scope global tun0
> valid_lft forever preferred_lft forever
>
> $ ip route show
> default via 192.168.100.2 dev tun0
> 192.168.0.0/24 dev eth0 proto kernel scope link src 192.168.0.2
> 192.168.100.0/24 dev tun0 proto kernel scope link src 192.168.100.1
>
[...]
> struct nf_flow_xmit xmit = {};
> + struct in6_addr *ip6_daddr;
> struct flow_offload *flow;
> struct neighbour *neigh;
> struct rtable *rt;
> @@ -844,25 +872,41 @@ nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
> other_tuple = &flow->tuplehash[!dir].tuple;
> ip_daddr = other_tuple->src_v4.s_addr;
>
> - if (nf_flow_tunnel_v4_push(state->net, skb, other_tuple, &ip_daddr) < 0)
> + if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
> + &ip6_daddr) < 0)
> return NF_DROP;
>
> switch (tuplehash->tuple.xmit_type) {
> - case FLOW_OFFLOAD_XMIT_NEIGH:
> - rt = dst_rtable(tuplehash->tuple.dst_cache);
> + case FLOW_OFFLOAD_XMIT_NEIGH: {
> + struct dst_entry *dst;
> +
> xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
> if (!xmit.outdev) {
> flow_offload_teardown(flow);
> return NF_DROP;
> }
> - neigh = ip_neigh_gw4(rt->dst.dev, rt_nexthop(rt, ip_daddr));
> + if (other_tuple->tun.encap_proto == AF_INET6 ||
> + ctx.tun.proto == IPPROTO_IPV6) {
> + struct rt6_info *rt6;
> +
> + rt6 = dst_rt6_info(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw6(rt6->dst.dev,
> + rt6_nexthop(rt6, ip6_daddr));
> + dst = &rt6->dst;
> + } else {
> + rt = dst_rtable(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw4(rt->dst.dev,
> + rt_nexthop(rt, ip_daddr));
> + dst = &rt->dst;
> + }
- Does this logic correctly handle cross-family tunneling like an IPv6-in-IPv4
packet (SIT) being forwarded out via another IPv4 tunnel?
- SIT support is added by a following patch in the series.
> if (IS_ERR(neigh)) {
> flow_offload_teardown(flow);
> return NF_DROP;
> }
> xmit.dest = neigh->ha;
> - skb_dst_set_noref(skb, &rt->dst);
> + skb_dst_set_noref(skb, dst);
> break;
> + }
> case FLOW_OFFLOAD_XMIT_DIRECT:
> xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
> if (!xmit.outdev) {
> @@ -1112,8 +1156,12 @@ nf_flow_offload_ipv6_lookup(struct nf_flowtable_ctx *ctx,
> if (!nf_flow_skb_encap_protocol(ctx, skb, htons(ETH_P_IPV6)))
> return NULL;
>
> - if (nf_flow_tuple_ipv6(ctx, skb, &tuple) < 0)
> + if (ctx->tun.proto == IPPROTO_IPIP) {
> + if (nf_flow_tuple_ip(ctx, skb, &tuple) < 0)
> + return NULL;
> + } else if (nf_flow_tuple_ipv6(ctx, skb, &tuple) < 0) {
> return NULL;
> + }
>
> return flow_offload_lookup(flow_table, &tuple);
> }
> @@ -1140,7 +1188,11 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> if (tuplehash == NULL)
> return NF_ACCEPT;
>
> - ret = nf_flow_offload_ipv6_forward(&ctx, flow_table, tuplehash, skb);
> + if (ctx.tun.proto == IPPROTO_IPIP)
> + ret = nf_flow_offload_forward(&ctx, flow_table, tuplehash, skb);
> + else
> + ret = nf_flow_offload_ipv6_forward(&ctx, flow_table, tuplehash,
> + skb);
> if (ret < 0)
> return NF_DROP;
> else if (ret == 0)
> @@ -1164,21 +1216,38 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> return NF_DROP;
>
> switch (tuplehash->tuple.xmit_type) {
> - case FLOW_OFFLOAD_XMIT_NEIGH:
> - rt = dst_rt6_info(tuplehash->tuple.dst_cache);
> + case FLOW_OFFLOAD_XMIT_NEIGH: {
> + struct dst_entry *dst;
> +
> xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
> if (!xmit.outdev) {
> flow_offload_teardown(flow);
> return NF_DROP;
> }
> - neigh = ip_neigh_gw6(rt->dst.dev, rt6_nexthop(rt, ip6_daddr));
> + if (other_tuple->tun.encap_proto == AF_INET ||
> + ctx.tun.proto == IPPROTO_IPIP) {
> + __be32 ip_daddr = other_tuple->src_v4.s_addr;
> + struct rtable *rt4;
> +
> + skb->protocol = htons(ETH_P_IP);
> + rt4 = dst_rtable(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw4(rt4->dst.dev,
> + rt_nexthop(rt4, ip_daddr));
> + dst = &rt4->dst;
- Can this branch incorrectly cast a struct rt6_info to a struct rtable
if an IPv4-in-IPv6 packet is forwarded to another IPv6 tunnel?
- Double tunnel encapsulation is not currently supported.
Regards,
Lorenzo
> + } else {
> + rt = dst_rt6_info(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw6(rt->dst.dev,
> + rt6_nexthop(rt, ip6_daddr));
> + dst = &rt->dst;
> + }
> if (IS_ERR(neigh)) {
> flow_offload_teardown(flow);
> return NF_DROP;
> }
> xmit.dest = neigh->ha;
> - skb_dst_set_noref(skb, &rt->dst);
> + skb_dst_set_noref(skb, dst);
> break;
> + }
> case FLOW_OFFLOAD_XMIT_DIRECT:
> xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
> if (!xmit.outdev) {
> diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
> index caaf48c5fd2a..5e84b7f18a26 100644
> --- a/net/netfilter/nf_flow_table_path.c
> +++ b/net/netfilter/nf_flow_table_path.c
> @@ -216,12 +216,13 @@ static int nft_flow_tunnel_update_route(const struct nft_pktinfo *pkt,
> struct dst_entry *tun_dst = NULL;
> struct flowi fl = {};
>
> - switch (nft_pf(pkt)) {
> + switch (tun->encap_proto) {
> case NFPROTO_IPV4:
> fl.u.ip4.daddr = tun->dst_v4.s_addr;
> fl.u.ip4.saddr = tun->src_v4.s_addr;
> fl.u.ip4.flowi4_iif = nft_in(pkt)->ifindex;
> - fl.u.ip4.flowi4_dscp = ip4h_dscp(ip_hdr(pkt->skb));
> + if (nft_pf(pkt) == NFPROTO_IPV4)
> + fl.u.ip4.flowi4_dscp = ip4h_dscp(ip_hdr(pkt->skb));
> fl.u.ip4.flowi4_mark = pkt->skb->mark;
> fl.u.ip4.flowi4_flags = FLOWI_FLAG_ANYSRC;
> break;
> @@ -229,13 +230,14 @@ static int nft_flow_tunnel_update_route(const struct nft_pktinfo *pkt,
> fl.u.ip6.daddr = tun->dst_v6;
> fl.u.ip6.saddr = tun->src_v6;
> fl.u.ip6.flowi6_iif = nft_in(pkt)->ifindex;
> - fl.u.ip6.flowlabel = ip6_flowinfo(ipv6_hdr(pkt->skb));
> + if (nft_pf(pkt) == NFPROTO_IPV6)
> + fl.u.ip6.flowlabel = ip6_flowinfo(ipv6_hdr(pkt->skb));
> fl.u.ip6.flowi6_mark = pkt->skb->mark;
> fl.u.ip6.flowi6_flags = FLOWI_FLAG_ANYSRC;
> break;
> }
>
> - nf_route(nft_net(pkt), &tun_dst, &fl, false, nft_pf(pkt));
> + nf_route(nft_net(pkt), &tun_dst, &fl, false, tun->encap_proto);
> if (!tun_dst)
> return -ENOENT;
>
>
> --
> 2.55.0
>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH nf-next v4 3/6] net: netfilter: Add IPv4 over IPv6 tunnel flowtable acceleration
2026-07-03 14:10 ` [PATCH nf-next v4 3/6] net: netfilter: Add IPv4 over IPv6 tunnel flowtable acceleration Lorenzo Bianconi
2026-07-07 16:25 ` Lorenzo Bianconi
@ 2026-07-08 10:11 ` Lorenzo Bianconi
1 sibling, 0 replies; 11+ messages in thread
From: Lorenzo Bianconi @ 2026-07-08 10:11 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Felix Fietkau, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, David Ahern,
Ido Schimmel, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
Shuah Khan
Cc: linux-arm-kernel, linux-mediatek, netdev, netfilter-devel,
coreteam, linux-kselftest
[-- Attachment #1: Type: text/plain, Size: 18997 bytes --]
commenting on sashiko's report:
https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260703-b4-flowtable-sw-accel-ip6ip-v4-0-00398cd12382%40kernel.org
> Introduce sw flowtable acceleration for the TX/RX paths of
> IPv4 over IPv6 tunnels, relying on the netfilter flowtable
> infrastructure.
> The feature can be tested with a forwarding scenario between two
> NICs (eth0 and eth1), where an IPv4 over IPv6 tunnel is used to
> reach a remote site via eth1 as the underlay device:
>
> ETH0 -- TUN0 <==> ETH1 -- [IP network] -- TUN1 (2001:db8:2::2)
>
> [IP configuration]
>
> 6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
> link/ether 00:00:22:33:11:55 brd ff:ff:ff:ff:ff:ff
> inet 192.168.0.2/24 scope global eth0
> valid_lft forever preferred_lft forever
> 7: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
> link/ether 00:11:22:33:11:55 brd ff:ff:ff:ff:ff:ff
> inet6 2001:db8:2::1/64 scope global nodad
> valid_lft forever preferred_lft forever
> 8: tun0@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
> link/tunnel6 2001:db8:2::1 peer 2001:db8:2::2 permaddr ce9c:2940:7dcc::
> inet 192.168.100.1/24 scope global tun0
> valid_lft forever preferred_lft forever
>
> $ ip route show
> default via 192.168.100.2 dev tun0
> 192.168.0.0/24 dev eth0 proto kernel scope link src 192.168.0.2
> 192.168.100.0/24 dev tun0 proto kernel scope link src 192.168.100.1
>
> $ ip -6 route show
> 2001:db8:2::/64 dev eth1 proto kernel metric 256 pref medium
>
> $ nft list ruleset
> table inet filter {
> flowtable ft {
> hook ingress priority filter
> devices = { eth0, eth1 }
> }
>
> chain forward {
> type filter hook forward priority filter; policy accept;
> meta l4proto { tcp, udp } flow add @ft
> }
> }
>
> When reproducing this scenario using veth interfaces, the following
> results were observed:
>
> - TCP stream received from IPv4 over IPv6 tunnel:
> - net-next (baseline): ~126 Gbps
> - net-next + IP6IP flowtable support: ~138 Gbps
>
> - TCP stream transmitted to IPv4 over IPv6 tunnel:
> - net-next (baseline): ~127 Gbps
> - net-next + IP6IP flowtable support: ~140 Gbps
>
> Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
> ---
> net/netfilter/nf_flow_table_core.c | 16 +++-
> net/netfilter/nf_flow_table_ip.c | 161 ++++++++++++++++++++++++++-----------
> net/netfilter/nf_flow_table_path.c | 10 ++-
> 3 files changed, 133 insertions(+), 54 deletions(-)
>
> diff --git a/net/netfilter/nf_flow_table_core.c b/net/netfilter/nf_flow_table_core.c
> index 99c5b9d671a0..18f89e6fb435 100644
> --- a/net/netfilter/nf_flow_table_core.c
> +++ b/net/netfilter/nf_flow_table_core.c
> @@ -76,9 +76,14 @@ struct flow_offload *flow_offload_alloc(struct nf_conn *ct)
> }
> EXPORT_SYMBOL_GPL(flow_offload_alloc);
>
> -static u32 flow_offload_dst_cookie(struct flow_offload_tuple *flow_tuple)
> +static u32 flow_offload_dst_cookie(struct flow_offload_tuple *flow_tuple,
> + u8 tun_encap_proto)
> {
> - if (flow_tuple->l3proto == NFPROTO_IPV6)
> + bool dst_v6;
> +
> + dst_v6 = tun_encap_proto ? tun_encap_proto == NFPROTO_IPV6
> + : flow_tuple->l3proto == NFPROTO_IPV6;
> + if (dst_v6)
> return rt6_get_cookie(dst_rt6_info(flow_tuple->dst_cache));
>
> return 0;
> @@ -99,10 +104,12 @@ static int flow_offload_fill_route(struct flow_offload *flow,
> enum flow_offload_tuple_dir dir)
> {
> struct flow_offload_tuple *flow_tuple = &flow->tuplehash[dir].tuple;
> + u8 l3proto, encap_proto = route->tuple[!dir].in.tun.encap_proto;
> struct dst_entry *dst = nft_route_dst_fetch(route, dir);
> int i, j = 0;
>
> - switch (flow_tuple->l3proto) {
> + l3proto = encap_proto ? encap_proto : flow_tuple->l3proto;
> + switch (l3proto) {
> case NFPROTO_IPV4:
> flow_tuple->mtu = ip_dst_mtu_maybe_forward(dst, true);
> break;
> @@ -138,7 +145,8 @@ static int flow_offload_fill_route(struct flow_offload *flow,
> case FLOW_OFFLOAD_XMIT_NEIGH:
> flow_tuple->ifidx = route->tuple[dir].out.ifindex;
> flow_tuple->dst_cache = dst;
> - flow_tuple->dst_cookie = flow_offload_dst_cookie(flow_tuple);
> + flow_tuple->dst_cookie = flow_offload_dst_cookie(flow_tuple,
> + encap_proto);
> break;
> default:
> WARN_ON_ONCE(1);
> diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c
> index cf2c74e3fd56..4b6de16bd4f3 100644
> --- a/net/netfilter/nf_flow_table_ip.c
> +++ b/net/netfilter/nf_flow_table_ip.c
> @@ -191,27 +191,27 @@ static void nf_flow_tuple_encap(struct nf_flowtable_ctx *ctx,
> break;
> }
>
> - switch (inner_proto) {
> - case htons(ETH_P_IP):
> - iph = (struct iphdr *)(skb_network_header(skb) + offset);
> - if (ctx->tun.proto == IPPROTO_IPIP) {
> + if (ctx->tun.proto == IPPROTO_IPIP || ctx->tun.proto == IPPROTO_IPV6) {
> + switch (inner_proto) {
> + case htons(ETH_P_IP):
> + iph = (struct iphdr *)(skb_network_header(skb) +
> + offset);
> tuple->tun.dst_v4.s_addr = iph->daddr;
> tuple->tun.src_v4.s_addr = iph->saddr;
> - tuple->tun.l3_proto = IPPROTO_IPIP;
> + tuple->tun.l3_proto = ctx->tun.proto;
> tuple->tun.encap_proto = AF_INET;
> - }
> - break;
> - case htons(ETH_P_IPV6):
> - ip6h = (struct ipv6hdr *)(skb_network_header(skb) + offset);
> - if (ctx->tun.proto == IPPROTO_IPV6) {
> + break;
> + case htons(ETH_P_IPV6):
> + ip6h = (struct ipv6hdr *)(skb_network_header(skb) +
> + offset);
> tuple->tun.dst_v6 = ip6h->daddr;
> tuple->tun.src_v6 = ip6h->saddr;
> - tuple->tun.l3_proto = IPPROTO_IPV6;
> + tuple->tun.l3_proto = ctx->tun.proto;
> tuple->tun.encap_proto = AF_INET6;
> + break;
> + default:
> + break;
> }
> - break;
> - default:
> - break;
> }
> }
>
> @@ -363,7 +363,7 @@ static bool nf_flow_ip6_tunnel_proto(struct nf_flowtable_ctx *ctx,
> if (ipv6_ext_hdr(ip6h->nexthdr))
> return false;
>
> - if (ip6h->nexthdr == IPPROTO_IPV6) {
> + if (ip6h->nexthdr == IPPROTO_IPIP || ip6h->nexthdr == IPPROTO_IPV6) {
> ctx->tun.proto = ip6h->nexthdr;
> ctx->tun.hdr_size = sizeof(*ip6h);
> ctx->offset += ctx->tun.hdr_size;
> @@ -384,6 +384,10 @@ static void nf_flow_ip_tunnel_pop(struct nf_flowtable_ctx *ctx,
>
> skb_pull(skb, ctx->tun.hdr_size);
> skb_reset_network_header(skb);
> + if (ctx->tun.proto == IPPROTO_IPIP)
> + skb->protocol = htons(ETH_P_IP);
> + else
> + skb->protocol = htons(ETH_P_IPV6);
> }
>
> static bool nf_flow_skb_encap_protocol(struct nf_flowtable_ctx *ctx,
> @@ -489,8 +493,16 @@ static int nf_flow_offload_forward(struct nf_flowtable_ctx *ctx,
> flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
>
> mtu = flow->tuplehash[dir].tuple.mtu + ctx->offset;
> - if (flow->tuplehash[!dir].tuple.tun_num)
> + switch (flow->tuplehash[!dir].tuple.tun.encap_proto) {
> + case AF_INET:
> mtu -= sizeof(*iph);
> + break;
> + case AF_INET6:
> + mtu -= sizeof(struct ipv6hdr);
> + break;
> + default:
> + break;
> + }
>
> if (unlikely(nf_flow_exceeds_mtu(skb, mtu)))
> return 0;
> @@ -636,56 +648,57 @@ static int nf_flow_tunnel_ipip_push(struct net *net, struct sk_buff *skb,
> return 0;
> }
>
> -static int nf_flow_tunnel_v4_push(struct net *net, struct sk_buff *skb,
> - struct flow_offload_tuple *tuple,
> - __be32 *ip_daddr)
> -{
> - if (tuple->tun_num)
> - return nf_flow_tunnel_ipip_push(net, skb, tuple, ip_daddr);
> -
> - return 0;
> -}
> -
> static int nf_flow_tunnel_ip6ip6_push(struct net *net, struct sk_buff *skb,
> struct flow_offload_tuple *tuple,
> struct in6_addr **ip6_daddr)
- This helper is still named nf_flow_tunnel_ip6ip6_push() but now also
encapsulates IPv4 inner packets inside an IPv6 outer header, branching on
tuple->tun.l3_proto and writing that value into ip6h->nexthdr rather than
hard-coding IPPROTO_IPV6.
Would a name like nf_flow_tunnel_ip6_push() (or splitting the inner-family
preamble into a small helper) better reflect that this now covers both
6-in-6 and 4-in-6 encapsulation?
- I will fix it in v5.
> {
> - struct ipv6hdr *ip6h = (struct ipv6hdr *)skb_network_header(skb);
> - struct rtable *rt = dst_rtable(tuple->dst_cache);
> - __u8 dsfield = ipv6_get_dsfield(ip6h);
> + struct dst_entry *dst = tuple->dst_cache;
> struct flowi6 fl6 = {
> .daddr = tuple->tun.src_v6,
> .saddr = tuple->tun.dst_v6,
> .flowi6_proto = IPPROTO_IPV6,
> };
> - u8 hop_limit = ip6h->hop_limit;
> + u8 hop_limit, dsfield;
> + struct ipv6hdr *ip6h;
> int err, mtu;
> u32 headroom;
>
> + if (tuple->tun.l3_proto == IPPROTO_IPIP) {
> + struct iphdr *iph = (struct iphdr *)skb_network_header(skb);
> +
> + dsfield = ipv4_get_dsfield(iph);
> + hop_limit = iph->ttl;
> + } else {
> + ip6h = (struct ipv6hdr *)skb_network_header(skb);
> + dsfield = ipv6_get_dsfield(ip6h);
> + hop_limit = ip6h->hop_limit;
> + }
> +
> err = iptunnel_handle_offloads(skb, SKB_GSO_IPXIP6);
> if (err)
> return err;
>
> - skb_set_inner_ipproto(skb, IPPROTO_IPV6);
> - headroom = sizeof(*ip6h) + LL_RESERVED_SPACE(rt->dst.dev) +
> - rt->dst.header_len;
> + skb_set_inner_ipproto(skb, tuple->tun.l3_proto);
> + headroom = sizeof(*ip6h) + LL_RESERVED_SPACE(dst->dev) +
> + dst->header_len;
> err = skb_cow_head(skb, headroom);
> if (err)
> return err;
>
> skb_scrub_packet(skb, true);
> - mtu = dst_mtu(&rt->dst) - sizeof(*ip6h);
> + mtu = dst_mtu(dst) - sizeof(*ip6h);
> mtu = max(mtu, IPV6_MIN_MTU);
> skb_dst_update_pmtu_no_confirm(skb, mtu);
>
> skb_push(skb, sizeof(*ip6h));
> skb_reset_network_header(skb);
> + skb->protocol = htons(ETH_P_IPV6);
>
> ip6h = ipv6_hdr(skb);
> ip6_flow_hdr(ip6h, dsfield,
> ip6_make_flowlabel(net, skb, fl6.flowlabel, true, &fl6));
> ip6h->hop_limit = hop_limit;
> - ip6h->nexthdr = IPPROTO_IPV6;
> + ip6h->nexthdr = tuple->tun.l3_proto;
> ip6h->daddr = tuple->tun.src_v6;
> ip6h->saddr = tuple->tun.dst_v6;
> ipv6_hdr(skb)->payload_len = htons(skb->len - sizeof(*ip6h));
> @@ -696,6 +709,20 @@ static int nf_flow_tunnel_ip6ip6_push(struct net *net, struct sk_buff *skb,
> return 0;
> }
>
> +static int nf_flow_tunnel_push(struct net *net, struct sk_buff *skb,
> + struct flow_offload_tuple *tuple,
> + __be32 *ip_daddr, struct in6_addr **ip6_daddr)
> +{
> + switch (tuple->tun.encap_proto) {
> + case AF_INET:
> + return nf_flow_tunnel_ipip_push(net, skb, tuple, ip_daddr);
> + case AF_INET6:
> + return nf_flow_tunnel_ip6ip6_push(net, skb, tuple, ip6_daddr);
> + default:
> + return 0;
> + }
> +}
> +
> static int nf_flow_tunnel_v6_push(struct net *net, struct sk_buff *skb,
> struct flow_offload_tuple *tuple,
> struct in6_addr **ip6_daddr)
- The IPv4 hook now goes through the family-aware nf_flow_tunnel_push()
dispatcher while the IPv6 hook still calls the narrow
nf_flow_tunnel_v6_push() wrapper below.
Would it be preferable to route both hooks through nf_flow_tunnel_push()
so that the two entry points share the same abstraction?
- I will fix it in v5.
> @@ -815,6 +842,7 @@ nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
> .in = state->in,
> };
> struct nf_flow_xmit xmit = {};
> + struct in6_addr *ip6_daddr;
> struct flow_offload *flow;
> struct neighbour *neigh;
> struct rtable *rt;
> @@ -844,25 +872,41 @@ nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
> other_tuple = &flow->tuplehash[!dir].tuple;
> ip_daddr = other_tuple->src_v4.s_addr;
>
> - if (nf_flow_tunnel_v4_push(state->net, skb, other_tuple, &ip_daddr) < 0)
> + if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
> + &ip6_daddr) < 0)
- ip6_daddr is left uninitialized here, then its address is passed to
nf_flow_tunnel_push() below. nf_flow_tunnel_push() only writes through
ip6_daddr in the AF_INET6 case; the AF_INET and default cases leave the
pointer untouched:
switch (tuple->tun.encap_proto) {
case AF_INET:
return nf_flow_tunnel_ipip_push(net, skb, tuple, ip_daddr);
case AF_INET6:
return nf_flow_tunnel_ip6ip6_push(net, skb, tuple, ip6_daddr);
default:
return 0;
}
The symmetric IPv6 hook explicitly pre-initialises the corresponding
pointer with ip6_daddr = &other_tuple->src_v6 before the push call. Should
the IPv4 hook mirror that pattern here?
- I will fix it in v5.
> return NF_DROP;
>
> switch (tuplehash->tuple.xmit_type) {
> - case FLOW_OFFLOAD_XMIT_NEIGH:
> - rt = dst_rtable(tuplehash->tuple.dst_cache);
> + case FLOW_OFFLOAD_XMIT_NEIGH: {
> + struct dst_entry *dst;
> +
> xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
> if (!xmit.outdev) {
> flow_offload_teardown(flow);
> return NF_DROP;
> }
> - neigh = ip_neigh_gw4(rt->dst.dev, rt_nexthop(rt, ip_daddr));
> + if (other_tuple->tun.encap_proto == AF_INET6 ||
> + ctx.tun.proto == IPPROTO_IPV6) {
> + struct rt6_info *rt6;
> +
> + rt6 = dst_rt6_info(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw6(rt6->dst.dev,
> + rt6_nexthop(rt6, ip6_daddr));
> + dst = &rt6->dst;
> + } else {
> + rt = dst_rtable(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw4(rt->dst.dev,
> + rt_nexthop(rt, ip_daddr));
> + dst = &rt->dst;
> + }
> if (IS_ERR(neigh)) {
> flow_offload_teardown(flow);
> return NF_DROP;
> }
> xmit.dest = neigh->ha;
> - skb_dst_set_noref(skb, &rt->dst);
> + skb_dst_set_noref(skb, dst);
> break;
> + }
> case FLOW_OFFLOAD_XMIT_DIRECT:
> xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
> if (!xmit.outdev) {
> @@ -1112,8 +1156,12 @@ nf_flow_offload_ipv6_lookup(struct nf_flowtable_ctx *ctx,
> if (!nf_flow_skb_encap_protocol(ctx, skb, htons(ETH_P_IPV6)))
> return NULL;
>
> - if (nf_flow_tuple_ipv6(ctx, skb, &tuple) < 0)
> + if (ctx->tun.proto == IPPROTO_IPIP) {
> + if (nf_flow_tuple_ip(ctx, skb, &tuple) < 0)
> + return NULL;
> + } else if (nf_flow_tuple_ipv6(ctx, skb, &tuple) < 0) {
> return NULL;
> + }
>
> return flow_offload_lookup(flow_table, &tuple);
> }
> @@ -1140,7 +1188,11 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> if (tuplehash == NULL)
> return NF_ACCEPT;
>
> - ret = nf_flow_offload_ipv6_forward(&ctx, flow_table, tuplehash, skb);
> + if (ctx.tun.proto == IPPROTO_IPIP)
> + ret = nf_flow_offload_forward(&ctx, flow_table, tuplehash, skb);
> + else
> + ret = nf_flow_offload_ipv6_forward(&ctx, flow_table, tuplehash,
> + skb);
> if (ret < 0)
> return NF_DROP;
> else if (ret == 0)
> @@ -1164,21 +1216,38 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> return NF_DROP;
>
> switch (tuplehash->tuple.xmit_type) {
> - case FLOW_OFFLOAD_XMIT_NEIGH:
> - rt = dst_rt6_info(tuplehash->tuple.dst_cache);
> + case FLOW_OFFLOAD_XMIT_NEIGH: {
> + struct dst_entry *dst;
> +
> xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
> if (!xmit.outdev) {
> flow_offload_teardown(flow);
> return NF_DROP;
> }
> - neigh = ip_neigh_gw6(rt->dst.dev, rt6_nexthop(rt, ip6_daddr));
> + if (other_tuple->tun.encap_proto == AF_INET ||
> + ctx.tun.proto == IPPROTO_IPIP) {
> + __be32 ip_daddr = other_tuple->src_v4.s_addr;
> + struct rtable *rt4;
> +
> + skb->protocol = htons(ETH_P_IP);
> + rt4 = dst_rtable(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw4(rt4->dst.dev,
> + rt_nexthop(rt4, ip_daddr));
> + dst = &rt4->dst;
> + } else {
> + rt = dst_rt6_info(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw6(rt->dst.dev,
> + rt6_nexthop(rt, ip6_daddr));
> + dst = &rt->dst;
> + }
> if (IS_ERR(neigh)) {
> flow_offload_teardown(flow);
> return NF_DROP;
> }
> xmit.dest = neigh->ha;
> - skb_dst_set_noref(skb, &rt->dst);
> + skb_dst_set_noref(skb, dst);
> break;
> + }
> case FLOW_OFFLOAD_XMIT_DIRECT:
> xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
> if (!xmit.outdev) {
> diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
> index caaf48c5fd2a..5e84b7f18a26 100644
> --- a/net/netfilter/nf_flow_table_path.c
> +++ b/net/netfilter/nf_flow_table_path.c
> @@ -216,12 +216,13 @@ static int nft_flow_tunnel_update_route(const struct nft_pktinfo *pkt,
> struct dst_entry *tun_dst = NULL;
> struct flowi fl = {};
>
> - switch (nft_pf(pkt)) {
> + switch (tun->encap_proto) {
> case NFPROTO_IPV4:
> fl.u.ip4.daddr = tun->dst_v4.s_addr;
> fl.u.ip4.saddr = tun->src_v4.s_addr;
> fl.u.ip4.flowi4_iif = nft_in(pkt)->ifindex;
> - fl.u.ip4.flowi4_dscp = ip4h_dscp(ip_hdr(pkt->skb));
> + if (nft_pf(pkt) == NFPROTO_IPV4)
> + fl.u.ip4.flowi4_dscp = ip4h_dscp(ip_hdr(pkt->skb));
> fl.u.ip4.flowi4_mark = pkt->skb->mark;
> fl.u.ip4.flowi4_flags = FLOWI_FLAG_ANYSRC;
> break;
> @@ -229,13 +230,14 @@ static int nft_flow_tunnel_update_route(const struct nft_pktinfo *pkt,
> fl.u.ip6.daddr = tun->dst_v6;
> fl.u.ip6.saddr = tun->src_v6;
> fl.u.ip6.flowi6_iif = nft_in(pkt)->ifindex;
> - fl.u.ip6.flowlabel = ip6_flowinfo(ipv6_hdr(pkt->skb));
> + if (nft_pf(pkt) == NFPROTO_IPV6)
> + fl.u.ip6.flowlabel = ip6_flowinfo(ipv6_hdr(pkt->skb));
> fl.u.ip6.flowi6_mark = pkt->skb->mark;
> fl.u.ip6.flowi6_flags = FLOWI_FLAG_ANYSRC;
> break;
- This changes route-family selection from nft_pf(pkt) to tun->encap_proto
and gates the DSCP and flowlabel inputs on nft_pf(pkt) matching the outer
family, so cross-family flows now pass zero DSCP or flowlabel into the
flowi used for the FIB lookup.
Setups that use policy routing keyed on TOS/DSCP or flow label may see
different route selection than before. The changelog describes the
IPv4-over-IPv6 feature and its benchmarks but does not mention this
behavioural change on tunnel route selection.
Could this behavioural change be called out in the commit message so it
is discoverable during bisection and stable-tree evaluation?
- I will fix commit logs in v5.
Regards,
Lorenzo
> }
>
> - nf_route(nft_net(pkt), &tun_dst, &fl, false, nft_pf(pkt));
> + nf_route(nft_net(pkt), &tun_dst, &fl, false, tun->encap_proto);
> if (!tun_dst)
> return -ENOENT;
>
>
> --
> 2.55.0
>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH nf-next v4 4/6] selftests: netfilter: nft_flowtable.sh: Add IPv4 over IPv6 flowtable selftest
2026-07-03 14:10 [PATCH nf-next v4 0/6] Add IPv4 over IPv6 and SIT flowtable SW acceleration Lorenzo Bianconi
` (2 preceding siblings ...)
2026-07-03 14:10 ` [PATCH nf-next v4 3/6] net: netfilter: Add IPv4 over IPv6 tunnel flowtable acceleration Lorenzo Bianconi
@ 2026-07-03 14:10 ` Lorenzo Bianconi
2026-07-03 14:10 ` [PATCH nf-next v4 5/6] net: netfilter: Add SIT tunnel flowtable acceleration Lorenzo Bianconi
2026-07-03 14:10 ` [PATCH nf-next v4 6/6] selftests: netfilter: nft_flowtable.sh: Add SIT flowtable selftest Lorenzo Bianconi
5 siblings, 0 replies; 11+ messages in thread
From: Lorenzo Bianconi @ 2026-07-03 14:10 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Felix Fietkau, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, David Ahern,
Ido Schimmel, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
Shuah Khan, Lorenzo Bianconi
Cc: linux-arm-kernel, linux-mediatek, netdev, netfilter-devel,
coreteam, linux-kselftest
Similar to IPIP and IP6IP6, introduce specific selftest for IPv4 over IPv6
flowtable sw acceleration in nft_flowtable.sh
Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
---
.../selftests/net/netfilter/nft_flowtable.sh | 33 +++++++++++++++++++---
1 file changed, 29 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/net/netfilter/nft_flowtable.sh b/tools/testing/selftests/net/netfilter/nft_flowtable.sh
index 08ad07500e8a..b14c80c6e372 100755
--- a/tools/testing/selftests/net/netfilter/nft_flowtable.sh
+++ b/tools/testing/selftests/net/netfilter/nft_flowtable.sh
@@ -579,9 +579,8 @@ if ! test_tcp_forwarding_nat "$ns1" "$ns2" 1 ""; then
ip netns exec "$nsr1" nft list ruleset
fi
-# IPIP tunnel test:
-# Add IPIP tunnel interfaces and check flowtable acceleration.
-test_ipip() {
+# IP tunnel tests:
+test_ip_tnls() {
if ! ip -net "$nsr1" link add name tun0 type ipip \
local 192.168.10.1 remote 192.168.10.2 >/dev/null;then
echo "SKIP: could not add ipip tunnel"
@@ -594,7 +593,9 @@ ip netns exec "$nsr1" sysctl net.ipv4.conf.tun0.forwarding=1 > /dev/null
ip -net "$nsr1" link add name tun6 type ip6tnl local fee1:2::1 remote fee1:2::2 encaplimit none
ip -net "$nsr1" link set tun6 up
+ip -net "$nsr1" addr add 192.168.210.1/24 dev tun6
ip -net "$nsr1" addr add fee1:3::1/64 dev tun6 nodad
+ip netns exec "$nsr1" sysctl net.ipv4.conf.tun6.forwarding=1 > /dev/null
ip -net "$nsr2" link add name tun0 type ipip local 192.168.10.2 remote 192.168.10.1
ip -net "$nsr2" link set tun0 up
@@ -603,7 +604,9 @@ ip netns exec "$nsr2" sysctl net.ipv4.conf.tun0.forwarding=1 > /dev/null
ip -net "$nsr2" link add name tun6 type ip6tnl local fee1:2::2 remote fee1:2::1 encaplimit none || ret=1
ip -net "$nsr2" link set tun6 up
+ip -net "$nsr2" addr add 192.168.210.2/24 dev tun6
ip -net "$nsr2" addr add fee1:3::2/64 dev tun6 nodad
+ip netns exec "$nsr2" sysctl net.ipv4.conf.tun6.forwarding=1 > /dev/null
ip -net "$nsr1" route change default via 192.168.100.2
ip -net "$nsr2" route change default via 192.168.100.1
@@ -636,6 +639,15 @@ else
ret=1
fi
+ip -net "$nsr1" route change default via 192.168.210.2
+ip -net "$nsr2" route change default via 192.168.210.1
+
+if ! test_tcp_forwarding_nat "$ns1" "$ns2" 1 "IP4IP6 tunnel"; then
+ echo "FAIL: flow offload for ns1/ns2 with IP4IP6 tunnel" 1>&2
+ ip netns exec "$nsr1" nft list ruleset
+ ret=1
+fi
+
# Create vlan tagged devices for IPIP traffic.
ip -net "$nsr1" link add link veth1 name veth1.10 type vlan id 10
ip -net "$nsr1" link set veth1.10 up
@@ -653,7 +665,9 @@ ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif tun0.10 a
ip -net "$nsr1" link add name tun6.10 type ip6tnl local fee1:4::1 remote fee1:4::2 encaplimit none
ip -net "$nsr1" link set tun6.10 up
+ip -net "$nsr1" addr add 192.168.220.1/24 dev tun6.10
ip -net "$nsr1" addr add fee1:5::1/64 dev tun6.10 nodad
+ip netns exec "$nsr1" sysctl net.ipv4.conf.tun6/10.forwarding=1 > /dev/null
ip -6 -net "$nsr1" route delete default
ip -6 -net "$nsr1" route add default via fee1:5::2
ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif tun6.10 accept'
@@ -672,7 +686,9 @@ ip netns exec "$nsr2" sysctl net.ipv4.conf.tun0/10.forwarding=1 > /dev/null
ip -net "$nsr2" link add name tun6.10 type ip6tnl local fee1:4::2 remote fee1:4::1 encaplimit none || ret=1
ip -net "$nsr2" link set tun6.10 up
+ip -net "$nsr2" addr add 192.168.220.2/24 dev tun6.10
ip -net "$nsr2" addr add fee1:5::2/64 dev tun6.10 nodad
+ip netns exec "$nsr2" sysctl net.ipv4.conf.tun6/10.forwarding=1 > /dev/null
ip -6 -net "$nsr2" route delete default
ip -6 -net "$nsr2" route add default via fee1:5::1
@@ -690,6 +706,15 @@ else
ret=1
fi
+ip -net "$nsr1" route change default via 192.168.220.2
+ip -net "$nsr2" route change default via 192.168.220.1
+
+if ! test_tcp_forwarding_nat "$ns1" "$ns2" 1 "IP4IP6 tunnel over vlan"; then
+ echo "FAIL: flow offload for ns1/ns2 with IP4IP6 tunnel over vlan" 1>&2
+ ip netns exec "$nsr1" nft list ruleset
+ ret=1
+fi
+
# Restore the previous configuration
ip -net "$nsr1" route change default via 192.168.10.2
ip -net "$nsr2" route change default via 192.168.10.1
@@ -782,7 +807,7 @@ ip -net "$nsr1" addr add dead:1::1/64 dev veth0 nodad
ip -net "$nsr1" link set up dev veth0
}
-test_ipip
+test_ip_tnls
test_bridge
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH nf-next v4 5/6] net: netfilter: Add SIT tunnel flowtable acceleration
2026-07-03 14:10 [PATCH nf-next v4 0/6] Add IPv4 over IPv6 and SIT flowtable SW acceleration Lorenzo Bianconi
` (3 preceding siblings ...)
2026-07-03 14:10 ` [PATCH nf-next v4 4/6] selftests: netfilter: nft_flowtable.sh: Add IPv4 over IPv6 flowtable selftest Lorenzo Bianconi
@ 2026-07-03 14:10 ` Lorenzo Bianconi
2026-07-08 8:17 ` Lorenzo Bianconi
2026-07-08 10:33 ` Lorenzo Bianconi
2026-07-03 14:10 ` [PATCH nf-next v4 6/6] selftests: netfilter: nft_flowtable.sh: Add SIT flowtable selftest Lorenzo Bianconi
5 siblings, 2 replies; 11+ messages in thread
From: Lorenzo Bianconi @ 2026-07-03 14:10 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Felix Fietkau, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, David Ahern,
Ido Schimmel, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
Shuah Khan, Lorenzo Bianconi
Cc: linux-arm-kernel, linux-mediatek, netdev, netfilter-devel,
coreteam, linux-kselftest
Introduce sw flowtable acceleration for the TX/RX paths of
SIT tunnels, relying on the netfilter flowtable infrastructure.
The feature can be tested with a forwarding scenario between two
NICs (eth0 and eth1), where a SIT tunnel is used to reach a remote
site via eth1 as the underlay device:
ETH0 -- TUN0 <==> ETH1 -- [IP network] -- TUN1 (192.168.2.2)
[IP configuration]
6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 00:00:22:33:11:55 brd ff:ff:ff:ff:ff:ff
inet6 2001:db8:1::2/64 scope global nodad
valid_lft forever preferred_lft forever
7: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 00:11:22:33:11:55 brd ff:ff:ff:ff:ff:ff
inet 192.168.2.1/24 scope global eth1
valid_lft forever preferred_lft forever
8: tun0@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
link/sit 192.168.2.1 peer 192.168.2.2
inet6 2001:db8:200::1/64 scope global nodad
valid_lft forever preferred_lft forever
$ ip route show
192.168.2.0/24 dev eth1 proto kernel scope link src 192.168.2.1
$ ip -6 route show
2001:db8:1::/64 dev eth0 proto kernel metric 256 pref medium
2001:db8:200::/64 dev tun0 proto kernel metric 256 pref medium
default via 2001:db8:200::2 dev tun0 metric 1024 pref medium
$ nft list ruleset
table inet filter {
flowtable ft {
hook ingress priority filter
devices = { eth0, eth1 }
}
chain forward {
type filter hook forward priority filter; policy accept;
meta l4proto { tcp, udp } flow add @ft
}
}
When reproducing this scenario using veth interfaces, the following
results were observed:
- TCP stream received from SIT tunnel:
- net-next (baseline): ~118 Gbps
- net-next + SIT flowtable support: ~148 Gbps
- TCP stream transmitted to SIT tunnel:
- net-next (baseline): ~131 Gbps
- net-next + SIT flowtable support: ~147 Gbps
Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
---
net/ipv6/sit.c | 26 ++++
net/netfilter/nf_flow_table_ip.c | 290 +++++++++++++++++++++------------------
2 files changed, 182 insertions(+), 134 deletions(-)
diff --git a/net/ipv6/sit.c b/net/ipv6/sit.c
index a38b24fb8384..0ac6f7839878 100644
--- a/net/ipv6/sit.c
+++ b/net/ipv6/sit.c
@@ -1365,6 +1365,31 @@ ipip6_tunnel_ctl(struct net_device *dev, struct ip_tunnel_parm_kern *p,
}
}
+static int ipip6_tunnel_fill_forward_path(struct net_device_path_ctx *ctx,
+ struct net_device_path *path)
+{
+ struct ip_tunnel *tunnel = netdev_priv(ctx->dev);
+ const struct iphdr *tiph = &tunnel->parms.iph;
+ struct rtable *rt;
+
+ rt = ip_route_output(dev_net(ctx->dev), tiph->daddr, 0, 0, 0,
+ RT_SCOPE_UNIVERSE);
+ if (IS_ERR(rt))
+ return PTR_ERR(rt);
+
+ path->type = DEV_PATH_TUN;
+ path->tun.src_v4.s_addr = tiph->saddr;
+ path->tun.dst_v4.s_addr = tiph->daddr;
+ path->tun.l3_proto = IPPROTO_IPV6;
+ path->tun.encap_proto = AF_INET;
+ path->dev = ctx->dev;
+
+ ctx->dev = rt->dst.dev;
+ ip_rt_put(rt);
+
+ return 0;
+}
+
static int
ipip6_tunnel_siocdevprivate(struct net_device *dev, struct ifreq *ifr,
void __user *data, int cmd)
@@ -1401,6 +1426,7 @@ static const struct net_device_ops ipip6_netdev_ops = {
.ndo_siocdevprivate = ipip6_tunnel_siocdevprivate,
.ndo_get_iflink = ip_tunnel_get_iflink,
.ndo_tunnel_ctl = ipip6_tunnel_ctl,
+ .ndo_fill_forward_path = ipip6_tunnel_fill_forward_path,
};
static void ipip6_dev_free(struct net_device *dev)
diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c
index 4b6de16bd4f3..f02e71ae5448 100644
--- a/net/netfilter/nf_flow_table_ip.c
+++ b/net/netfilter/nf_flow_table_ip.c
@@ -338,7 +338,7 @@ static bool nf_flow_ip4_tunnel_proto(struct nf_flowtable_ctx *ctx,
if (iph->ttl <= 1)
return false;
- if (iph->protocol == IPPROTO_IPIP) {
+ if (iph->protocol == IPPROTO_IPIP || iph->protocol == IPPROTO_IPV6) {
ctx->tun.proto = iph->protocol;
ctx->tun.hdr_size = size;
ctx->offset += ctx->tun.hdr_size;
@@ -464,21 +464,6 @@ static void nf_flow_encap_pop(struct nf_flowtable_ctx *ctx,
nf_flow_ip_tunnel_pop(ctx, skb);
}
-static struct flow_offload_tuple_rhash *
-nf_flow_offload_lookup(struct nf_flowtable_ctx *ctx,
- struct nf_flowtable *flow_table, struct sk_buff *skb)
-{
- struct flow_offload_tuple tuple = {};
-
- if (!nf_flow_skb_encap_protocol(ctx, skb, htons(ETH_P_IP)))
- return NULL;
-
- if (nf_flow_tuple_ip(ctx, skb, &tuple) < 0)
- return NULL;
-
- return flow_offload_lookup(flow_table, &tuple);
-}
-
static int nf_flow_offload_forward(struct nf_flowtable_ctx *ctx,
struct nf_flowtable *flow_table,
struct flow_offload_tuple_rhash *tuplehash,
@@ -606,19 +591,33 @@ static int nf_flow_tunnel_ipip_push(struct net *net, struct sk_buff *skb,
struct flow_offload_tuple *tuple,
__be32 *ip_daddr)
{
- struct iphdr *iph = (struct iphdr *)skb_network_header(skb);
struct rtable *rt = dst_rtable(tuple->dst_cache);
- u8 tos = iph->tos, ttl = iph->ttl;
- __be16 frag_off = iph->frag_off;
- u32 headroom = sizeof(*iph);
+ __be16 frag_off = 0;
+ struct iphdr *iph;
+ u8 tos = 0, ttl;
+ u32 headroom;
int err;
+ if (tuple->tun.l3_proto == IPPROTO_IPV6) {
+ struct ipv6hdr *ip6h;
+
+ ip6h = (struct ipv6hdr *)skb_network_header(skb);
+ tos = ipv6_get_dsfield(ip6h);
+ ttl = ip6h->hop_limit;
+ } else {
+ iph = (struct iphdr *)skb_network_header(skb);
+ frag_off = iph->frag_off;
+ tos = iph->tos;
+ ttl = iph->ttl;
+ }
+
err = iptunnel_handle_offloads(skb, SKB_GSO_IPXIP4);
if (err)
return err;
- skb_set_inner_ipproto(skb, IPPROTO_IPIP);
- headroom += LL_RESERVED_SPACE(rt->dst.dev) + rt->dst.header_len;
+ skb_set_inner_ipproto(skb, tuple->tun.l3_proto);
+ headroom = sizeof(*iph) + LL_RESERVED_SPACE(rt->dst.dev) +
+ rt->dst.header_len;
err = skb_cow_head(skb, headroom);
if (err)
return err;
@@ -629,6 +628,7 @@ static int nf_flow_tunnel_ipip_push(struct net *net, struct sk_buff *skb,
/* Push down and install the IP header. */
skb_push(skb, sizeof(*iph));
skb_reset_network_header(skb);
+ skb->protocol = htons(ETH_P_IP);
iph = ip_hdr(skb);
iph->version = 4;
@@ -723,16 +723,6 @@ static int nf_flow_tunnel_push(struct net *net, struct sk_buff *skb,
}
}
-static int nf_flow_tunnel_v6_push(struct net *net, struct sk_buff *skb,
- struct flow_offload_tuple *tuple,
- struct in6_addr **ip6_daddr)
-{
- if (tuple->tun_num)
- return nf_flow_tunnel_ip6ip6_push(net, skb, tuple, ip6_daddr);
-
- return 0;
-}
-
static int nf_flow_encap_push(struct sk_buff *skb,
struct flow_offload_tuple *tuple,
struct net_device *outdev)
@@ -830,103 +820,6 @@ static unsigned int nf_flow_queue_xmit(struct net *net, struct sk_buff *skb,
return NF_STOLEN;
}
-unsigned int
-nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
- const struct nf_hook_state *state)
-{
- struct flow_offload_tuple_rhash *tuplehash;
- struct nf_flowtable *flow_table = priv;
- struct flow_offload_tuple *other_tuple;
- enum flow_offload_tuple_dir dir;
- struct nf_flowtable_ctx ctx = {
- .in = state->in,
- };
- struct nf_flow_xmit xmit = {};
- struct in6_addr *ip6_daddr;
- struct flow_offload *flow;
- struct neighbour *neigh;
- struct rtable *rt;
- __be32 ip_daddr;
- int ret;
-
- tuplehash = nf_flow_offload_lookup(&ctx, flow_table, skb);
- if (!tuplehash)
- return NF_ACCEPT;
-
- ret = nf_flow_offload_forward(&ctx, flow_table, tuplehash, skb);
- if (ret < 0)
- return NF_DROP;
- else if (ret == 0)
- return NF_ACCEPT;
-
- if (unlikely(tuplehash->tuple.xmit_type == FLOW_OFFLOAD_XMIT_XFRM)) {
- rt = dst_rtable(tuplehash->tuple.dst_cache);
- memset(skb->cb, 0, sizeof(struct inet_skb_parm));
- IPCB(skb)->iif = skb->dev->ifindex;
- IPCB(skb)->flags = IPSKB_FORWARDED;
- return nf_flow_xmit_xfrm(skb, state, &rt->dst);
- }
-
- dir = tuplehash->tuple.dir;
- flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
- other_tuple = &flow->tuplehash[!dir].tuple;
- ip_daddr = other_tuple->src_v4.s_addr;
-
- if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
- &ip6_daddr) < 0)
- return NF_DROP;
-
- switch (tuplehash->tuple.xmit_type) {
- case FLOW_OFFLOAD_XMIT_NEIGH: {
- struct dst_entry *dst;
-
- xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
- if (!xmit.outdev) {
- flow_offload_teardown(flow);
- return NF_DROP;
- }
- if (other_tuple->tun.encap_proto == AF_INET6 ||
- ctx.tun.proto == IPPROTO_IPV6) {
- struct rt6_info *rt6;
-
- rt6 = dst_rt6_info(tuplehash->tuple.dst_cache);
- neigh = ip_neigh_gw6(rt6->dst.dev,
- rt6_nexthop(rt6, ip6_daddr));
- dst = &rt6->dst;
- } else {
- rt = dst_rtable(tuplehash->tuple.dst_cache);
- neigh = ip_neigh_gw4(rt->dst.dev,
- rt_nexthop(rt, ip_daddr));
- dst = &rt->dst;
- }
- if (IS_ERR(neigh)) {
- flow_offload_teardown(flow);
- return NF_DROP;
- }
- xmit.dest = neigh->ha;
- skb_dst_set_noref(skb, dst);
- break;
- }
- case FLOW_OFFLOAD_XMIT_DIRECT:
- xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
- if (!xmit.outdev) {
- flow_offload_teardown(flow);
- return NF_DROP;
- }
- xmit.dest = tuplehash->tuple.out.h_dest;
- xmit.source = tuplehash->tuple.out.h_source;
- break;
- default:
- WARN_ON_ONCE(1);
- return NF_DROP;
- }
- xmit.tuple = other_tuple;
- xmit.needs_gso_segment = tuplehash->tuple.needs_gso_segment;
-
- return nf_flow_queue_xmit(state->net, skb, &xmit);
-}
-EXPORT_SYMBOL_GPL(nf_flow_offload_ip_hook);
-
static void nf_flow_nat_ipv6_tcp(struct sk_buff *skb, unsigned int thoff,
struct in6_addr *addr,
struct in6_addr *new_addr,
@@ -1111,8 +1004,16 @@ static int nf_flow_offload_ipv6_forward(struct nf_flowtable_ctx *ctx,
flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
mtu = flow->tuplehash[dir].tuple.mtu + ctx->offset;
- if (flow->tuplehash[!dir].tuple.tun_num)
+ switch (flow->tuplehash[!dir].tuple.tun.encap_proto) {
+ case AF_INET:
+ mtu -= sizeof(struct iphdr);
+ break;
+ case AF_INET6:
mtu -= sizeof(*ip6h);
+ break;
+ default:
+ break;
+ }
if (unlikely(nf_flow_exceeds_mtu(skb, mtu)))
return 0;
@@ -1146,6 +1047,25 @@ static int nf_flow_offload_ipv6_forward(struct nf_flowtable_ctx *ctx,
return 1;
}
+static struct flow_offload_tuple_rhash *
+nf_flow_offload_lookup(struct nf_flowtable_ctx *ctx,
+ struct nf_flowtable *flow_table, struct sk_buff *skb)
+{
+ struct flow_offload_tuple tuple = {};
+
+ if (!nf_flow_skb_encap_protocol(ctx, skb, htons(ETH_P_IP)))
+ return NULL;
+
+ if (ctx->tun.proto == IPPROTO_IPV6) {
+ if (nf_flow_tuple_ipv6(ctx, skb, &tuple) < 0)
+ return NULL;
+ } else if (nf_flow_tuple_ip(ctx, skb, &tuple) < 0) {
+ return NULL;
+ }
+
+ return flow_offload_lookup(flow_table, &tuple);
+}
+
static struct flow_offload_tuple_rhash *
nf_flow_offload_ipv6_lookup(struct nf_flowtable_ctx *ctx,
struct nf_flowtable *flow_table,
@@ -1166,6 +1086,108 @@ nf_flow_offload_ipv6_lookup(struct nf_flowtable_ctx *ctx,
return flow_offload_lookup(flow_table, &tuple);
}
+unsigned int
+nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
+ const struct nf_hook_state *state)
+{
+ struct flow_offload_tuple_rhash *tuplehash;
+ struct nf_flowtable *flow_table = priv;
+ struct flow_offload_tuple *other_tuple;
+ enum flow_offload_tuple_dir dir;
+ struct nf_flowtable_ctx ctx = {
+ .in = state->in,
+ };
+ struct nf_flow_xmit xmit = {};
+ struct in6_addr *ip6_daddr;
+ struct flow_offload *flow;
+ struct neighbour *neigh;
+ struct rtable *rt;
+ __be32 ip_daddr;
+ int ret;
+
+ tuplehash = nf_flow_offload_lookup(&ctx, flow_table, skb);
+ if (!tuplehash)
+ return NF_ACCEPT;
+
+ if (ctx.tun.proto == IPPROTO_IPV6)
+ ret = nf_flow_offload_ipv6_forward(&ctx, flow_table, tuplehash,
+ skb);
+ else
+ ret = nf_flow_offload_forward(&ctx, flow_table, tuplehash, skb);
+ if (ret < 0)
+ return NF_DROP;
+ else if (ret == 0)
+ return NF_ACCEPT;
+
+ if (unlikely(tuplehash->tuple.xmit_type == FLOW_OFFLOAD_XMIT_XFRM)) {
+ rt = dst_rtable(tuplehash->tuple.dst_cache);
+ memset(skb->cb, 0, sizeof(struct inet_skb_parm));
+ IPCB(skb)->iif = skb->dev->ifindex;
+ IPCB(skb)->flags = IPSKB_FORWARDED;
+ return nf_flow_xmit_xfrm(skb, state, &rt->dst);
+ }
+
+ dir = tuplehash->tuple.dir;
+ flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
+ other_tuple = &flow->tuplehash[!dir].tuple;
+ ip_daddr = other_tuple->src_v4.s_addr;
+ ip6_daddr = &other_tuple->src_v6;
+
+ if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
+ &ip6_daddr) < 0)
+ return NF_DROP;
+
+ switch (tuplehash->tuple.xmit_type) {
+ case FLOW_OFFLOAD_XMIT_NEIGH: {
+ struct dst_entry *dst;
+
+ xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
+ if (!xmit.outdev) {
+ flow_offload_teardown(flow);
+ return NF_DROP;
+ }
+ if (other_tuple->tun.encap_proto == AF_INET6 ||
+ ctx.tun.proto == IPPROTO_IPV6) {
+ struct rt6_info *rt6;
+
+ rt6 = dst_rt6_info(tuplehash->tuple.dst_cache);
+ neigh = ip_neigh_gw6(rt6->dst.dev,
+ rt6_nexthop(rt6, ip6_daddr));
+ dst = &rt6->dst;
+ } else {
+ rt = dst_rtable(tuplehash->tuple.dst_cache);
+ neigh = ip_neigh_gw4(rt->dst.dev,
+ rt_nexthop(rt, ip_daddr));
+ dst = &rt->dst;
+ }
+ if (IS_ERR(neigh)) {
+ flow_offload_teardown(flow);
+ return NF_DROP;
+ }
+ xmit.dest = neigh->ha;
+ skb_dst_set_noref(skb, dst);
+ break;
+ }
+ case FLOW_OFFLOAD_XMIT_DIRECT:
+ xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
+ if (!xmit.outdev) {
+ flow_offload_teardown(flow);
+ return NF_DROP;
+ }
+ xmit.dest = tuplehash->tuple.out.h_dest;
+ xmit.source = tuplehash->tuple.out.h_source;
+ break;
+ default:
+ WARN_ON_ONCE(1);
+ return NF_DROP;
+ }
+ xmit.tuple = other_tuple;
+ xmit.needs_gso_segment = tuplehash->tuple.needs_gso_segment;
+
+ return nf_flow_queue_xmit(state->net, skb, &xmit);
+}
+EXPORT_SYMBOL_GPL(nf_flow_offload_ip_hook);
+
unsigned int
nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
const struct nf_hook_state *state)
@@ -1182,6 +1204,7 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
struct flow_offload *flow;
struct neighbour *neigh;
struct rt6_info *rt;
+ __be32 ip_daddr;
int ret;
tuplehash = nf_flow_offload_ipv6_lookup(&ctx, flow_table, skb);
@@ -1209,10 +1232,11 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
dir = tuplehash->tuple.dir;
flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
other_tuple = &flow->tuplehash[!dir].tuple;
+ ip_daddr = other_tuple->src_v4.s_addr;
ip6_daddr = &other_tuple->src_v6;
- if (nf_flow_tunnel_v6_push(state->net, skb, other_tuple,
- &ip6_daddr) < 0)
+ if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
+ &ip6_daddr) < 0)
return NF_DROP;
switch (tuplehash->tuple.xmit_type) {
@@ -1226,10 +1250,8 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
}
if (other_tuple->tun.encap_proto == AF_INET ||
ctx.tun.proto == IPPROTO_IPIP) {
- __be32 ip_daddr = other_tuple->src_v4.s_addr;
struct rtable *rt4;
- skb->protocol = htons(ETH_P_IP);
rt4 = dst_rtable(tuplehash->tuple.dst_cache);
neigh = ip_neigh_gw4(rt4->dst.dev,
rt_nexthop(rt4, ip_daddr));
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH nf-next v4 5/6] net: netfilter: Add SIT tunnel flowtable acceleration
2026-07-03 14:10 ` [PATCH nf-next v4 5/6] net: netfilter: Add SIT tunnel flowtable acceleration Lorenzo Bianconi
@ 2026-07-08 8:17 ` Lorenzo Bianconi
2026-07-08 10:33 ` Lorenzo Bianconi
1 sibling, 0 replies; 11+ messages in thread
From: Lorenzo Bianconi @ 2026-07-08 8:17 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Felix Fietkau, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, David Ahern,
Ido Schimmel, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
Shuah Khan
Cc: linux-arm-kernel, linux-mediatek, netdev, netfilter-devel,
coreteam, linux-kselftest
[-- Attachment #1: Type: text/plain, Size: 18900 bytes --]
> Introduce sw flowtable acceleration for the TX/RX paths of
> SIT tunnels, relying on the netfilter flowtable infrastructure.
> The feature can be tested with a forwarding scenario between two
> NICs (eth0 and eth1), where a SIT tunnel is used to reach a remote
> site via eth1 as the underlay device:
>
> ETH0 -- TUN0 <==> ETH1 -- [IP network] -- TUN1 (192.168.2.2)
>
> [IP configuration]
>
> 6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
> link/ether 00:00:22:33:11:55 brd ff:ff:ff:ff:ff:ff
> inet6 2001:db8:1::2/64 scope global nodad
> valid_lft forever preferred_lft forever
> 7: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
> link/ether 00:11:22:33:11:55 brd ff:ff:ff:ff:ff:ff
> inet 192.168.2.1/24 scope global eth1
> valid_lft forever preferred_lft forever
> 8: tun0@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
> link/sit 192.168.2.1 peer 192.168.2.2
> inet6 2001:db8:200::1/64 scope global nodad
> valid_lft forever preferred_lft forever
commenting on sashiko's report:
https://sashiko.dev/#/patchset/20260703-b4-flowtable-sw-accel-ip6ip-v4-0-00398cd12382%40kernel.org
>
> $ ip route show
> 192.168.2.0/24 dev eth1 proto kernel scope link src 192.168.2.1
>
> $ ip -6 route show
> 2001:db8:1::/64 dev eth0 proto kernel metric 256 pref medium
> 2001:db8:200::/64 dev tun0 proto kernel metric 256 pref medium
> default via 2001:db8:200::2 dev tun0 metric 1024 pref medium
>
> $ nft list ruleset
> table inet filter {
> flowtable ft {
> hook ingress priority filter
> devices = { eth0, eth1 }
> }
>
> chain forward {
> type filter hook forward priority filter; policy accept;
> meta l4proto { tcp, udp } flow add @ft
> }
> }
>
> When reproducing this scenario using veth interfaces, the following
> results were observed:
>
> - TCP stream received from SIT tunnel:
> - net-next (baseline): ~118 Gbps
> - net-next + SIT flowtable support: ~148 Gbps
>
> - TCP stream transmitted to SIT tunnel:
> - net-next (baseline): ~131 Gbps
> - net-next + SIT flowtable support: ~147 Gbps
>
> Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
> ---
> net/ipv6/sit.c | 26 ++++
> net/netfilter/nf_flow_table_ip.c | 290 +++++++++++++++++++++------------------
> 2 files changed, 182 insertions(+), 134 deletions(-)
>
> diff --git a/net/ipv6/sit.c b/net/ipv6/sit.c
> index a38b24fb8384..0ac6f7839878 100644
> --- a/net/ipv6/sit.c
> +++ b/net/ipv6/sit.c
> @@ -1365,6 +1365,31 @@ ipip6_tunnel_ctl(struct net_device *dev, struct ip_tunnel_parm_kern *p,
> }
> }
>
> +static int ipip6_tunnel_fill_forward_path(struct net_device_path_ctx *ctx,
> + struct net_device_path *path)
> +{
> + struct ip_tunnel *tunnel = netdev_priv(ctx->dev);
> + const struct iphdr *tiph = &tunnel->parms.iph;
> + struct rtable *rt;
> +
> + rt = ip_route_output(dev_net(ctx->dev), tiph->daddr, 0, 0, 0,
> + RT_SCOPE_UNIVERSE);
- Does this handle NBMA (ISATAP/6to4/6rd) SIT tunnels correctly? For these
tunnels, tiph->daddr is typically 0 since the remote destination is resolved
per-packet.
If we unconditionally query the routing table here with tiph->daddr, it
will route to 0.0.0.0, causing the flow offload tuple to use 0.0.0.0 as the
outer encapsulation destination. Could this result in all offloaded traffic
for these tunnels being silently dropped?
- I will fix it in v5
> + if (IS_ERR(rt))
> + return PTR_ERR(rt);
> +
> + path->type = DEV_PATH_TUN;
> + path->tun.src_v4.s_addr = tiph->saddr;
> + path->tun.dst_v4.s_addr = tiph->daddr;
> + path->tun.l3_proto = IPPROTO_IPV6;
- Is it safe to hardcode path->tun.l3_proto to IPPROTO_IPV6 here?
SIT tunnels can also support IPv4 payloads. If an IPv4 flow is offloaded
over a SIT tunnel, the flow tuple will erroneously record the payload as
IPv6. During encapsulation, nf_flow_tunnel_ipip_push() might then parse
the inner IPv4 header as an IPv6 header, which could corrupt TOS and TTL
extraction.
- I will fix it in v5
> + path->tun.encap_proto = AF_INET;
> + path->dev = ctx->dev;
> +
> + ctx->dev = rt->dst.dev;
> + ip_rt_put(rt);
> +
> + return 0;
> +}
> +
> static int
> ipip6_tunnel_siocdevprivate(struct net_device *dev, struct ifreq *ifr,
> void __user *data, int cmd)
> @@ -1401,6 +1426,7 @@ static const struct net_device_ops ipip6_netdev_ops = {
> .ndo_siocdevprivate = ipip6_tunnel_siocdevprivate,
> .ndo_get_iflink = ip_tunnel_get_iflink,
> .ndo_tunnel_ctl = ipip6_tunnel_ctl,
> + .ndo_fill_forward_path = ipip6_tunnel_fill_forward_path,
> };
>
> static void ipip6_dev_free(struct net_device *dev)
> diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c
> index 4b6de16bd4f3..f02e71ae5448 100644
> --- a/net/netfilter/nf_flow_table_ip.c
> +++ b/net/netfilter/nf_flow_table_ip.c
> @@ -338,7 +338,7 @@ static bool nf_flow_ip4_tunnel_proto(struct nf_flowtable_ctx *ctx,
> if (iph->ttl <= 1)
> return false;
>
> - if (iph->protocol == IPPROTO_IPIP) {
> + if (iph->protocol == IPPROTO_IPIP || iph->protocol == IPPROTO_IPV6) {
> ctx->tun.proto = iph->protocol;
> ctx->tun.hdr_size = size;
> ctx->offset += ctx->tun.hdr_size;
> @@ -464,21 +464,6 @@ static void nf_flow_encap_pop(struct nf_flowtable_ctx *ctx,
> nf_flow_ip_tunnel_pop(ctx, skb);
> }
>
> -static struct flow_offload_tuple_rhash *
> -nf_flow_offload_lookup(struct nf_flowtable_ctx *ctx,
> - struct nf_flowtable *flow_table, struct sk_buff *skb)
> -{
> - struct flow_offload_tuple tuple = {};
> -
> - if (!nf_flow_skb_encap_protocol(ctx, skb, htons(ETH_P_IP)))
> - return NULL;
> -
> - if (nf_flow_tuple_ip(ctx, skb, &tuple) < 0)
> - return NULL;
> -
> - return flow_offload_lookup(flow_table, &tuple);
> -}
> -
> static int nf_flow_offload_forward(struct nf_flowtable_ctx *ctx,
> struct nf_flowtable *flow_table,
> struct flow_offload_tuple_rhash *tuplehash,
> @@ -606,19 +591,33 @@ static int nf_flow_tunnel_ipip_push(struct net *net, struct sk_buff *skb,
> struct flow_offload_tuple *tuple,
> __be32 *ip_daddr)
> {
> - struct iphdr *iph = (struct iphdr *)skb_network_header(skb);
> struct rtable *rt = dst_rtable(tuple->dst_cache);
> - u8 tos = iph->tos, ttl = iph->ttl;
> - __be16 frag_off = iph->frag_off;
> - u32 headroom = sizeof(*iph);
> + __be16 frag_off = 0;
> + struct iphdr *iph;
> + u8 tos = 0, ttl;
> + u32 headroom;
> int err;
>
> + if (tuple->tun.l3_proto == IPPROTO_IPV6) {
> + struct ipv6hdr *ip6h;
> +
> + ip6h = (struct ipv6hdr *)skb_network_header(skb);
> + tos = ipv6_get_dsfield(ip6h);
> + ttl = ip6h->hop_limit;
> + } else {
> + iph = (struct iphdr *)skb_network_header(skb);
> + frag_off = iph->frag_off;
> + tos = iph->tos;
> + ttl = iph->ttl;
> + }
> +
> err = iptunnel_handle_offloads(skb, SKB_GSO_IPXIP4);
> if (err)
> return err;
>
> - skb_set_inner_ipproto(skb, IPPROTO_IPIP);
> - headroom += LL_RESERVED_SPACE(rt->dst.dev) + rt->dst.header_len;
> + skb_set_inner_ipproto(skb, tuple->tun.l3_proto);
> + headroom = sizeof(*iph) + LL_RESERVED_SPACE(rt->dst.dev) +
> + rt->dst.header_len;
- This isn't a bug introduced by this patch, but is it correct to calculate
the headroom using rt->dst.dev?
The rt variable is derived from the reply direction tuple, so rt->dst.dev
points to the internal ingress device (e.g. eth0) rather than the egress
underlay device (e.g. eth1).
If the physical egress device requires more headroom than the internal
device, this allocation might be too small and trigger an skb_under_panic
during encapsulation.
- This is already fixed by a Pablo's fix
> err = skb_cow_head(skb, headroom);
> if (err)
> return err;
> @@ -629,6 +628,7 @@ static int nf_flow_tunnel_ipip_push(struct net *net, struct sk_buff *skb,
> /* Push down and install the IP header. */
> skb_push(skb, sizeof(*iph));
> skb_reset_network_header(skb);
> + skb->protocol = htons(ETH_P_IP);
>
> iph = ip_hdr(skb);
> iph->version = 4;
> @@ -723,16 +723,6 @@ static int nf_flow_tunnel_push(struct net *net, struct sk_buff *skb,
> }
> }
>
> -static int nf_flow_tunnel_v6_push(struct net *net, struct sk_buff *skb,
> - struct flow_offload_tuple *tuple,
> - struct in6_addr **ip6_daddr)
> -{
> - if (tuple->tun_num)
> - return nf_flow_tunnel_ip6ip6_push(net, skb, tuple, ip6_daddr);
> -
> - return 0;
> -}
> -
> static int nf_flow_encap_push(struct sk_buff *skb,
> struct flow_offload_tuple *tuple,
> struct net_device *outdev)
> @@ -830,103 +820,6 @@ static unsigned int nf_flow_queue_xmit(struct net *net, struct sk_buff *skb,
> return NF_STOLEN;
> }
>
> -unsigned int
> -nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
> - const struct nf_hook_state *state)
> -{
> - struct flow_offload_tuple_rhash *tuplehash;
> - struct nf_flowtable *flow_table = priv;
> - struct flow_offload_tuple *other_tuple;
> - enum flow_offload_tuple_dir dir;
> - struct nf_flowtable_ctx ctx = {
> - .in = state->in,
> - };
> - struct nf_flow_xmit xmit = {};
> - struct in6_addr *ip6_daddr;
> - struct flow_offload *flow;
> - struct neighbour *neigh;
> - struct rtable *rt;
> - __be32 ip_daddr;
> - int ret;
> -
> - tuplehash = nf_flow_offload_lookup(&ctx, flow_table, skb);
> - if (!tuplehash)
> - return NF_ACCEPT;
> -
> - ret = nf_flow_offload_forward(&ctx, flow_table, tuplehash, skb);
> - if (ret < 0)
> - return NF_DROP;
> - else if (ret == 0)
> - return NF_ACCEPT;
> -
> - if (unlikely(tuplehash->tuple.xmit_type == FLOW_OFFLOAD_XMIT_XFRM)) {
> - rt = dst_rtable(tuplehash->tuple.dst_cache);
> - memset(skb->cb, 0, sizeof(struct inet_skb_parm));
> - IPCB(skb)->iif = skb->dev->ifindex;
> - IPCB(skb)->flags = IPSKB_FORWARDED;
> - return nf_flow_xmit_xfrm(skb, state, &rt->dst);
- Since ctx.tun.proto can now be IPPROTO_IPV6 for an inner IPv6 packet
encapsulated in IPv4, is it safe to unconditionally cast the route to
rtable and initialize IPCB(skb) here?
- This is not a real problem since IP tunnel flowtable acceleration is not
supported for FLOW_OFFLOAD_XMIT_XFRM xmit_type.
Regards,
Lorenzo
> - }
> -
> - dir = tuplehash->tuple.dir;
> - flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
> - other_tuple = &flow->tuplehash[!dir].tuple;
> - ip_daddr = other_tuple->src_v4.s_addr;
> -
> - if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
> - &ip6_daddr) < 0)
> - return NF_DROP;
> -
> - switch (tuplehash->tuple.xmit_type) {
> - case FLOW_OFFLOAD_XMIT_NEIGH: {
> - struct dst_entry *dst;
> -
> - xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
> - if (!xmit.outdev) {
> - flow_offload_teardown(flow);
> - return NF_DROP;
> - }
> - if (other_tuple->tun.encap_proto == AF_INET6 ||
> - ctx.tun.proto == IPPROTO_IPV6) {
> - struct rt6_info *rt6;
> -
> - rt6 = dst_rt6_info(tuplehash->tuple.dst_cache);
> - neigh = ip_neigh_gw6(rt6->dst.dev,
> - rt6_nexthop(rt6, ip6_daddr));
> - dst = &rt6->dst;
> - } else {
> - rt = dst_rtable(tuplehash->tuple.dst_cache);
> - neigh = ip_neigh_gw4(rt->dst.dev,
> - rt_nexthop(rt, ip_daddr));
> - dst = &rt->dst;
> - }
> - if (IS_ERR(neigh)) {
> - flow_offload_teardown(flow);
> - return NF_DROP;
> - }
> - xmit.dest = neigh->ha;
> - skb_dst_set_noref(skb, dst);
> - break;
> - }
> - case FLOW_OFFLOAD_XMIT_DIRECT:
> - xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
> - if (!xmit.outdev) {
> - flow_offload_teardown(flow);
> - return NF_DROP;
> - }
> - xmit.dest = tuplehash->tuple.out.h_dest;
> - xmit.source = tuplehash->tuple.out.h_source;
> - break;
> - default:
> - WARN_ON_ONCE(1);
> - return NF_DROP;
> - }
> - xmit.tuple = other_tuple;
> - xmit.needs_gso_segment = tuplehash->tuple.needs_gso_segment;
> -
> - return nf_flow_queue_xmit(state->net, skb, &xmit);
> -}
> -EXPORT_SYMBOL_GPL(nf_flow_offload_ip_hook);
> -
> static void nf_flow_nat_ipv6_tcp(struct sk_buff *skb, unsigned int thoff,
> struct in6_addr *addr,
> struct in6_addr *new_addr,
> @@ -1111,8 +1004,16 @@ static int nf_flow_offload_ipv6_forward(struct nf_flowtable_ctx *ctx,
> flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
>
> mtu = flow->tuplehash[dir].tuple.mtu + ctx->offset;
> - if (flow->tuplehash[!dir].tuple.tun_num)
> + switch (flow->tuplehash[!dir].tuple.tun.encap_proto) {
> + case AF_INET:
> + mtu -= sizeof(struct iphdr);
> + break;
> + case AF_INET6:
> mtu -= sizeof(*ip6h);
> + break;
> + default:
> + break;
> + }
>
> if (unlikely(nf_flow_exceeds_mtu(skb, mtu)))
> return 0;
> @@ -1146,6 +1047,25 @@ static int nf_flow_offload_ipv6_forward(struct nf_flowtable_ctx *ctx,
> return 1;
> }
>
> +static struct flow_offload_tuple_rhash *
> +nf_flow_offload_lookup(struct nf_flowtable_ctx *ctx,
> + struct nf_flowtable *flow_table, struct sk_buff *skb)
> +{
> + struct flow_offload_tuple tuple = {};
> +
> + if (!nf_flow_skb_encap_protocol(ctx, skb, htons(ETH_P_IP)))
> + return NULL;
> +
> + if (ctx->tun.proto == IPPROTO_IPV6) {
> + if (nf_flow_tuple_ipv6(ctx, skb, &tuple) < 0)
> + return NULL;
> + } else if (nf_flow_tuple_ip(ctx, skb, &tuple) < 0) {
> + return NULL;
> + }
> +
> + return flow_offload_lookup(flow_table, &tuple);
> +}
> +
> static struct flow_offload_tuple_rhash *
> nf_flow_offload_ipv6_lookup(struct nf_flowtable_ctx *ctx,
> struct nf_flowtable *flow_table,
> @@ -1166,6 +1086,108 @@ nf_flow_offload_ipv6_lookup(struct nf_flowtable_ctx *ctx,
> return flow_offload_lookup(flow_table, &tuple);
> }
>
> +unsigned int
> +nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
> + const struct nf_hook_state *state)
> +{
> + struct flow_offload_tuple_rhash *tuplehash;
> + struct nf_flowtable *flow_table = priv;
> + struct flow_offload_tuple *other_tuple;
> + enum flow_offload_tuple_dir dir;
> + struct nf_flowtable_ctx ctx = {
> + .in = state->in,
> + };
> + struct nf_flow_xmit xmit = {};
> + struct in6_addr *ip6_daddr;
> + struct flow_offload *flow;
> + struct neighbour *neigh;
> + struct rtable *rt;
> + __be32 ip_daddr;
> + int ret;
> +
> + tuplehash = nf_flow_offload_lookup(&ctx, flow_table, skb);
> + if (!tuplehash)
> + return NF_ACCEPT;
> +
> + if (ctx.tun.proto == IPPROTO_IPV6)
> + ret = nf_flow_offload_ipv6_forward(&ctx, flow_table, tuplehash,
> + skb);
> + else
> + ret = nf_flow_offload_forward(&ctx, flow_table, tuplehash, skb);
> + if (ret < 0)
> + return NF_DROP;
> + else if (ret == 0)
> + return NF_ACCEPT;
> +
> + if (unlikely(tuplehash->tuple.xmit_type == FLOW_OFFLOAD_XMIT_XFRM)) {
> + rt = dst_rtable(tuplehash->tuple.dst_cache);
> + memset(skb->cb, 0, sizeof(struct inet_skb_parm));
> + IPCB(skb)->iif = skb->dev->ifindex;
> + IPCB(skb)->flags = IPSKB_FORWARDED;
> + return nf_flow_xmit_xfrm(skb, state, &rt->dst);
> + }
> +
> + dir = tuplehash->tuple.dir;
> + flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
> + other_tuple = &flow->tuplehash[!dir].tuple;
> + ip_daddr = other_tuple->src_v4.s_addr;
> + ip6_daddr = &other_tuple->src_v6;
> +
> + if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
> + &ip6_daddr) < 0)
> + return NF_DROP;
> +
> + switch (tuplehash->tuple.xmit_type) {
> + case FLOW_OFFLOAD_XMIT_NEIGH: {
> + struct dst_entry *dst;
> +
> + xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
> + if (!xmit.outdev) {
> + flow_offload_teardown(flow);
> + return NF_DROP;
> + }
> + if (other_tuple->tun.encap_proto == AF_INET6 ||
> + ctx.tun.proto == IPPROTO_IPV6) {
> + struct rt6_info *rt6;
> +
> + rt6 = dst_rt6_info(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw6(rt6->dst.dev,
> + rt6_nexthop(rt6, ip6_daddr));
> + dst = &rt6->dst;
> + } else {
> + rt = dst_rtable(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw4(rt->dst.dev,
> + rt_nexthop(rt, ip_daddr));
> + dst = &rt->dst;
> + }
> + if (IS_ERR(neigh)) {
> + flow_offload_teardown(flow);
> + return NF_DROP;
> + }
> + xmit.dest = neigh->ha;
> + skb_dst_set_noref(skb, dst);
> + break;
> + }
> + case FLOW_OFFLOAD_XMIT_DIRECT:
> + xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
> + if (!xmit.outdev) {
> + flow_offload_teardown(flow);
> + return NF_DROP;
> + }
> + xmit.dest = tuplehash->tuple.out.h_dest;
> + xmit.source = tuplehash->tuple.out.h_source;
> + break;
> + default:
> + WARN_ON_ONCE(1);
> + return NF_DROP;
> + }
> + xmit.tuple = other_tuple;
> + xmit.needs_gso_segment = tuplehash->tuple.needs_gso_segment;
> +
> + return nf_flow_queue_xmit(state->net, skb, &xmit);
> +}
> +EXPORT_SYMBOL_GPL(nf_flow_offload_ip_hook);
> +
> unsigned int
> nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> const struct nf_hook_state *state)
> @@ -1182,6 +1204,7 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> struct flow_offload *flow;
> struct neighbour *neigh;
> struct rt6_info *rt;
> + __be32 ip_daddr;
> int ret;
>
> tuplehash = nf_flow_offload_ipv6_lookup(&ctx, flow_table, skb);
> @@ -1209,10 +1232,11 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> dir = tuplehash->tuple.dir;
> flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
> other_tuple = &flow->tuplehash[!dir].tuple;
> + ip_daddr = other_tuple->src_v4.s_addr;
> ip6_daddr = &other_tuple->src_v6;
>
> - if (nf_flow_tunnel_v6_push(state->net, skb, other_tuple,
> - &ip6_daddr) < 0)
> + if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
> + &ip6_daddr) < 0)
> return NF_DROP;
>
> switch (tuplehash->tuple.xmit_type) {
> @@ -1226,10 +1250,8 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> }
> if (other_tuple->tun.encap_proto == AF_INET ||
> ctx.tun.proto == IPPROTO_IPIP) {
> - __be32 ip_daddr = other_tuple->src_v4.s_addr;
> struct rtable *rt4;
>
> - skb->protocol = htons(ETH_P_IP);
> rt4 = dst_rtable(tuplehash->tuple.dst_cache);
> neigh = ip_neigh_gw4(rt4->dst.dev,
> rt_nexthop(rt4, ip_daddr));
>
> --
> 2.55.0
>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH nf-next v4 5/6] net: netfilter: Add SIT tunnel flowtable acceleration
2026-07-03 14:10 ` [PATCH nf-next v4 5/6] net: netfilter: Add SIT tunnel flowtable acceleration Lorenzo Bianconi
2026-07-08 8:17 ` Lorenzo Bianconi
@ 2026-07-08 10:33 ` Lorenzo Bianconi
1 sibling, 0 replies; 11+ messages in thread
From: Lorenzo Bianconi @ 2026-07-08 10:33 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Felix Fietkau, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, David Ahern,
Ido Schimmel, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
Shuah Khan
Cc: linux-arm-kernel, linux-mediatek, netdev, netfilter-devel,
coreteam, linux-kselftest
[-- Attachment #1: Type: text/plain, Size: 20415 bytes --]
commenting on sashiko's report:
https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260703-b4-flowtable-sw-accel-ip6ip-v4-0-00398cd12382%40kernel.org
> Introduce sw flowtable acceleration for the TX/RX paths of
> SIT tunnels, relying on the netfilter flowtable infrastructure.
> The feature can be tested with a forwarding scenario between two
> NICs (eth0 and eth1), where a SIT tunnel is used to reach a remote
> site via eth1 as the underlay device:
>
> ETH0 -- TUN0 <==> ETH1 -- [IP network] -- TUN1 (192.168.2.2)
>
> [IP configuration]
>
> 6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
> link/ether 00:00:22:33:11:55 brd ff:ff:ff:ff:ff:ff
> inet6 2001:db8:1::2/64 scope global nodad
> valid_lft forever preferred_lft forever
> 7: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
> link/ether 00:11:22:33:11:55 brd ff:ff:ff:ff:ff:ff
> inet 192.168.2.1/24 scope global eth1
> valid_lft forever preferred_lft forever
> 8: tun0@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
> link/sit 192.168.2.1 peer 192.168.2.2
> inet6 2001:db8:200::1/64 scope global nodad
> valid_lft forever preferred_lft forever
>
> $ ip route show
> 192.168.2.0/24 dev eth1 proto kernel scope link src 192.168.2.1
>
> $ ip -6 route show
> 2001:db8:1::/64 dev eth0 proto kernel metric 256 pref medium
> 2001:db8:200::/64 dev tun0 proto kernel metric 256 pref medium
> default via 2001:db8:200::2 dev tun0 metric 1024 pref medium
>
> $ nft list ruleset
> table inet filter {
> flowtable ft {
> hook ingress priority filter
> devices = { eth0, eth1 }
> }
>
> chain forward {
> type filter hook forward priority filter; policy accept;
> meta l4proto { tcp, udp } flow add @ft
> }
> }
>
> When reproducing this scenario using veth interfaces, the following
> results were observed:
>
> - TCP stream received from SIT tunnel:
> - net-next (baseline): ~118 Gbps
> - net-next + SIT flowtable support: ~148 Gbps
>
> - TCP stream transmitted to SIT tunnel:
> - net-next (baseline): ~131 Gbps
> - net-next + SIT flowtable support: ~147 Gbps
>
> Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
> ---
> net/ipv6/sit.c | 26 ++++
> net/netfilter/nf_flow_table_ip.c | 290 +++++++++++++++++++++------------------
> 2 files changed, 182 insertions(+), 134 deletions(-)
>
> diff --git a/net/ipv6/sit.c b/net/ipv6/sit.c
> index a38b24fb8384..0ac6f7839878 100644
> --- a/net/ipv6/sit.c
> +++ b/net/ipv6/sit.c
> @@ -1365,6 +1365,31 @@ ipip6_tunnel_ctl(struct net_device *dev, struct ip_tunnel_parm_kern *p,
> }
> }
>
> +static int ipip6_tunnel_fill_forward_path(struct net_device_path_ctx *ctx,
> + struct net_device_path *path)
> +{
> + struct ip_tunnel *tunnel = netdev_priv(ctx->dev);
> + const struct iphdr *tiph = &tunnel->parms.iph;
> + struct rtable *rt;
> +
> + rt = ip_route_output(dev_net(ctx->dev), tiph->daddr, 0, 0, 0,
> + RT_SCOPE_UNIVERSE);
> + if (IS_ERR(rt))
> + return PTR_ERR(rt);
> +
> + path->type = DEV_PATH_TUN;
> + path->tun.src_v4.s_addr = tiph->saddr;
> + path->tun.dst_v4.s_addr = tiph->daddr;
> + path->tun.l3_proto = IPPROTO_IPV6;
> + path->tun.encap_proto = AF_INET;
> + path->dev = ctx->dev;
> +
> + ctx->dev = rt->dst.dev;
> + ip_rt_put(rt);
> +
> + return 0;
- Is tiph->daddr always meaningful here? SIT supports several modes where
tunnel->parms.iph.daddr is either zero or a placeholder and the real
outer IPv4 endpoint is derived per-packet from the inner IPv6
destination:
- ISATAP (dev->priv_flags & IFF_ISATAP), resolved by
ipip6_tunnel_dst_find(..., true)
- 6rd / 6to4, resolved per-packet via try_6rd() / check_6rd()
- NBMA / point-to-multipoint (tiph->daddr == 0), resolved by
ipip6_tunnel_dst_find(..., false)
In these cases ip_route_output(daddr=0) will fall through to the default
route, ctx->dev gets resolved to the wrong underlay, and every
accelerated packet is encapsulated toward 0.0.0.0 (or a fixed unicast
that does not match the flow) regardless of the actual inner
destination.
Compare ip6_tnl_fill_forward_path() which returns -EOPNOTSUPP when it
detects unsupported tunnel features; would something equivalent make
sense here, at least for !tiph->daddr, IFF_ISATAP, and 6rd configured
tunnels?
Does this path also need to consider tunnel->encap? SIT supports FOU
and GUE via ip_tunnel_encap(skb, &tunnel->encap, &protocol, &fl4) in the
slow path, and there is no rejection here when tunnel->encap.type is
not TUNNEL_ENCAP_NONE. Once a SIT tunnel that was configured with
encap fou/gue is attached to a flowtable, the fast path pushes a plain
IPv4 outer header via nf_flow_tunnel_ipip_push() and skips the FOU/GUE
encapsulation entirely. ip6_tnl_fill_forward_path() has an
-EOPNOTSUPP guard for its own analogous case (encaplimit); would a
similar guard work here?
The slow path in ipip6_tunnel_xmit() uses flowi4_init_output() with
tunnel->parms.link (oif), tunnel->fwmark, tos & INET_DSCP_MASK, and
tiph->saddr:
flowi4_init_output(&fl4, tunnel->parms.link, tunnel->fwmark,
tos & INET_DSCP_MASK, RT_SCOPE_UNIVERSE,
IPPROTO_IPV6, 0, dst, tiph->saddr, ...);
The call above passes 0 for saddr, dscp and oif and does not pass a
fwmark. For a SIT tunnel bound to a specific link, using fwmark-based
policy routing, or configured with a source address, can the fast-path
route lookup diverge from what the slow path would pick, so that
rt->dst.dev (which becomes ctx->dev and drives the flowtable egress)
ends up on a different underlay device than slow-path packets on the
same tunnel?
- I will fix it in v5.
- Is it safe to hardcode IPPROTO_IPV6 here? sit_tunnel_xmit() dispatches
on skb->protocol and legitimately supports ETH_P_IP (IPv4-in-IPv4 via
IPPROTO_IPIP) and, if CONFIG_MPLS is set, ETH_P_MPLS_UC (IPPROTO_MPLS);
ipip6_valid_ip_proto() also accepts both on the receive side.
If flowtable acceleration engages for a flow whose inner payload is
IPv4 or MPLS through a SIT device, the outer iph->protocol that
nf_flow_tunnel_ipip_push() synthesises from tuple->tun.l3_proto will be
IPPROTO_IPV6, and the receiver will drop or misparse the packet.
ip6_tnl_fill_forward_path() selects l3_proto from ctx->ether_type
(ETH_P_IP => IPPROTO_IPIP, else IPPROTO_IPV6) — would something similar
be appropriate here?
- I will fix it in v5.
Regards,
Lorenzo
> +}
> +
> static int
> ipip6_tunnel_siocdevprivate(struct net_device *dev, struct ifreq *ifr,
> void __user *data, int cmd)
> @@ -1401,6 +1426,7 @@ static const struct net_device_ops ipip6_netdev_ops = {
> .ndo_siocdevprivate = ipip6_tunnel_siocdevprivate,
> .ndo_get_iflink = ip_tunnel_get_iflink,
> .ndo_tunnel_ctl = ipip6_tunnel_ctl,
> + .ndo_fill_forward_path = ipip6_tunnel_fill_forward_path,
> };
>
> static void ipip6_dev_free(struct net_device *dev)
> diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c
> index 4b6de16bd4f3..f02e71ae5448 100644
> --- a/net/netfilter/nf_flow_table_ip.c
> +++ b/net/netfilter/nf_flow_table_ip.c
> @@ -338,7 +338,7 @@ static bool nf_flow_ip4_tunnel_proto(struct nf_flowtable_ctx *ctx,
> if (iph->ttl <= 1)
> return false;
>
> - if (iph->protocol == IPPROTO_IPIP) {
> + if (iph->protocol == IPPROTO_IPIP || iph->protocol == IPPROTO_IPV6) {
> ctx->tun.proto = iph->protocol;
> ctx->tun.hdr_size = size;
> ctx->offset += ctx->tun.hdr_size;
> @@ -464,21 +464,6 @@ static void nf_flow_encap_pop(struct nf_flowtable_ctx *ctx,
> nf_flow_ip_tunnel_pop(ctx, skb);
> }
>
> -static struct flow_offload_tuple_rhash *
> -nf_flow_offload_lookup(struct nf_flowtable_ctx *ctx,
> - struct nf_flowtable *flow_table, struct sk_buff *skb)
> -{
> - struct flow_offload_tuple tuple = {};
> -
> - if (!nf_flow_skb_encap_protocol(ctx, skb, htons(ETH_P_IP)))
> - return NULL;
> -
> - if (nf_flow_tuple_ip(ctx, skb, &tuple) < 0)
> - return NULL;
> -
> - return flow_offload_lookup(flow_table, &tuple);
> -}
> -
> static int nf_flow_offload_forward(struct nf_flowtable_ctx *ctx,
> struct nf_flowtable *flow_table,
> struct flow_offload_tuple_rhash *tuplehash,
> @@ -606,19 +591,33 @@ static int nf_flow_tunnel_ipip_push(struct net *net, struct sk_buff *skb,
> struct flow_offload_tuple *tuple,
> __be32 *ip_daddr)
> {
> - struct iphdr *iph = (struct iphdr *)skb_network_header(skb);
> struct rtable *rt = dst_rtable(tuple->dst_cache);
> - u8 tos = iph->tos, ttl = iph->ttl;
> - __be16 frag_off = iph->frag_off;
> - u32 headroom = sizeof(*iph);
> + __be16 frag_off = 0;
> + struct iphdr *iph;
> + u8 tos = 0, ttl;
> + u32 headroom;
> int err;
>
> + if (tuple->tun.l3_proto == IPPROTO_IPV6) {
> + struct ipv6hdr *ip6h;
> +
> + ip6h = (struct ipv6hdr *)skb_network_header(skb);
> + tos = ipv6_get_dsfield(ip6h);
> + ttl = ip6h->hop_limit;
> + } else {
> + iph = (struct iphdr *)skb_network_header(skb);
> + frag_off = iph->frag_off;
> + tos = iph->tos;
> + ttl = iph->ttl;
> + }
> +
> err = iptunnel_handle_offloads(skb, SKB_GSO_IPXIP4);
> if (err)
> return err;
>
> - skb_set_inner_ipproto(skb, IPPROTO_IPIP);
> - headroom += LL_RESERVED_SPACE(rt->dst.dev) + rt->dst.header_len;
> + skb_set_inner_ipproto(skb, tuple->tun.l3_proto);
> + headroom = sizeof(*iph) + LL_RESERVED_SPACE(rt->dst.dev) +
> + rt->dst.header_len;
> err = skb_cow_head(skb, headroom);
> if (err)
> return err;
> @@ -629,6 +628,7 @@ static int nf_flow_tunnel_ipip_push(struct net *net, struct sk_buff *skb,
> /* Push down and install the IP header. */
> skb_push(skb, sizeof(*iph));
> skb_reset_network_header(skb);
> + skb->protocol = htons(ETH_P_IP);
>
> iph = ip_hdr(skb);
> iph->version = 4;
> @@ -723,16 +723,6 @@ static int nf_flow_tunnel_push(struct net *net, struct sk_buff *skb,
> }
> }
>
> -static int nf_flow_tunnel_v6_push(struct net *net, struct sk_buff *skb,
> - struct flow_offload_tuple *tuple,
> - struct in6_addr **ip6_daddr)
> -{
> - if (tuple->tun_num)
> - return nf_flow_tunnel_ip6ip6_push(net, skb, tuple, ip6_daddr);
> -
> - return 0;
> -}
> -
> static int nf_flow_encap_push(struct sk_buff *skb,
> struct flow_offload_tuple *tuple,
> struct net_device *outdev)
> @@ -830,103 +820,6 @@ static unsigned int nf_flow_queue_xmit(struct net *net, struct sk_buff *skb,
> return NF_STOLEN;
> }
>
> -unsigned int
> -nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
> - const struct nf_hook_state *state)
> -{
> - struct flow_offload_tuple_rhash *tuplehash;
> - struct nf_flowtable *flow_table = priv;
> - struct flow_offload_tuple *other_tuple;
> - enum flow_offload_tuple_dir dir;
> - struct nf_flowtable_ctx ctx = {
> - .in = state->in,
> - };
> - struct nf_flow_xmit xmit = {};
> - struct in6_addr *ip6_daddr;
> - struct flow_offload *flow;
> - struct neighbour *neigh;
> - struct rtable *rt;
> - __be32 ip_daddr;
> - int ret;
> -
> - tuplehash = nf_flow_offload_lookup(&ctx, flow_table, skb);
> - if (!tuplehash)
> - return NF_ACCEPT;
> -
> - ret = nf_flow_offload_forward(&ctx, flow_table, tuplehash, skb);
> - if (ret < 0)
> - return NF_DROP;
> - else if (ret == 0)
> - return NF_ACCEPT;
> -
> - if (unlikely(tuplehash->tuple.xmit_type == FLOW_OFFLOAD_XMIT_XFRM)) {
> - rt = dst_rtable(tuplehash->tuple.dst_cache);
> - memset(skb->cb, 0, sizeof(struct inet_skb_parm));
> - IPCB(skb)->iif = skb->dev->ifindex;
> - IPCB(skb)->flags = IPSKB_FORWARDED;
> - return nf_flow_xmit_xfrm(skb, state, &rt->dst);
> - }
> -
> - dir = tuplehash->tuple.dir;
> - flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
> - other_tuple = &flow->tuplehash[!dir].tuple;
> - ip_daddr = other_tuple->src_v4.s_addr;
> -
> - if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
> - &ip6_daddr) < 0)
> - return NF_DROP;
> -
> - switch (tuplehash->tuple.xmit_type) {
> - case FLOW_OFFLOAD_XMIT_NEIGH: {
> - struct dst_entry *dst;
> -
> - xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
> - if (!xmit.outdev) {
> - flow_offload_teardown(flow);
> - return NF_DROP;
> - }
> - if (other_tuple->tun.encap_proto == AF_INET6 ||
> - ctx.tun.proto == IPPROTO_IPV6) {
> - struct rt6_info *rt6;
> -
> - rt6 = dst_rt6_info(tuplehash->tuple.dst_cache);
> - neigh = ip_neigh_gw6(rt6->dst.dev,
> - rt6_nexthop(rt6, ip6_daddr));
> - dst = &rt6->dst;
> - } else {
> - rt = dst_rtable(tuplehash->tuple.dst_cache);
> - neigh = ip_neigh_gw4(rt->dst.dev,
> - rt_nexthop(rt, ip_daddr));
> - dst = &rt->dst;
> - }
> - if (IS_ERR(neigh)) {
> - flow_offload_teardown(flow);
> - return NF_DROP;
> - }
> - xmit.dest = neigh->ha;
> - skb_dst_set_noref(skb, dst);
> - break;
> - }
> - case FLOW_OFFLOAD_XMIT_DIRECT:
> - xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
> - if (!xmit.outdev) {
> - flow_offload_teardown(flow);
> - return NF_DROP;
> - }
> - xmit.dest = tuplehash->tuple.out.h_dest;
> - xmit.source = tuplehash->tuple.out.h_source;
> - break;
> - default:
> - WARN_ON_ONCE(1);
> - return NF_DROP;
> - }
> - xmit.tuple = other_tuple;
> - xmit.needs_gso_segment = tuplehash->tuple.needs_gso_segment;
> -
> - return nf_flow_queue_xmit(state->net, skb, &xmit);
> -}
> -EXPORT_SYMBOL_GPL(nf_flow_offload_ip_hook);
> -
> static void nf_flow_nat_ipv6_tcp(struct sk_buff *skb, unsigned int thoff,
> struct in6_addr *addr,
> struct in6_addr *new_addr,
> @@ -1111,8 +1004,16 @@ static int nf_flow_offload_ipv6_forward(struct nf_flowtable_ctx *ctx,
> flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
>
> mtu = flow->tuplehash[dir].tuple.mtu + ctx->offset;
> - if (flow->tuplehash[!dir].tuple.tun_num)
> + switch (flow->tuplehash[!dir].tuple.tun.encap_proto) {
> + case AF_INET:
> + mtu -= sizeof(struct iphdr);
> + break;
> + case AF_INET6:
> mtu -= sizeof(*ip6h);
> + break;
> + default:
> + break;
> + }
>
> if (unlikely(nf_flow_exceeds_mtu(skb, mtu)))
> return 0;
> @@ -1146,6 +1047,25 @@ static int nf_flow_offload_ipv6_forward(struct nf_flowtable_ctx *ctx,
> return 1;
> }
>
> +static struct flow_offload_tuple_rhash *
> +nf_flow_offload_lookup(struct nf_flowtable_ctx *ctx,
> + struct nf_flowtable *flow_table, struct sk_buff *skb)
> +{
> + struct flow_offload_tuple tuple = {};
> +
> + if (!nf_flow_skb_encap_protocol(ctx, skb, htons(ETH_P_IP)))
> + return NULL;
> +
> + if (ctx->tun.proto == IPPROTO_IPV6) {
> + if (nf_flow_tuple_ipv6(ctx, skb, &tuple) < 0)
> + return NULL;
> + } else if (nf_flow_tuple_ip(ctx, skb, &tuple) < 0) {
> + return NULL;
> + }
> +
> + return flow_offload_lookup(flow_table, &tuple);
> +}
> +
> static struct flow_offload_tuple_rhash *
> nf_flow_offload_ipv6_lookup(struct nf_flowtable_ctx *ctx,
> struct nf_flowtable *flow_table,
> @@ -1166,6 +1086,108 @@ nf_flow_offload_ipv6_lookup(struct nf_flowtable_ctx *ctx,
> return flow_offload_lookup(flow_table, &tuple);
> }
>
> +unsigned int
> +nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
> + const struct nf_hook_state *state)
> +{
> + struct flow_offload_tuple_rhash *tuplehash;
> + struct nf_flowtable *flow_table = priv;
> + struct flow_offload_tuple *other_tuple;
> + enum flow_offload_tuple_dir dir;
> + struct nf_flowtable_ctx ctx = {
> + .in = state->in,
> + };
> + struct nf_flow_xmit xmit = {};
> + struct in6_addr *ip6_daddr;
> + struct flow_offload *flow;
> + struct neighbour *neigh;
> + struct rtable *rt;
> + __be32 ip_daddr;
> + int ret;
> +
> + tuplehash = nf_flow_offload_lookup(&ctx, flow_table, skb);
> + if (!tuplehash)
> + return NF_ACCEPT;
> +
> + if (ctx.tun.proto == IPPROTO_IPV6)
> + ret = nf_flow_offload_ipv6_forward(&ctx, flow_table, tuplehash,
> + skb);
> + else
> + ret = nf_flow_offload_forward(&ctx, flow_table, tuplehash, skb);
> + if (ret < 0)
> + return NF_DROP;
> + else if (ret == 0)
> + return NF_ACCEPT;
> +
> + if (unlikely(tuplehash->tuple.xmit_type == FLOW_OFFLOAD_XMIT_XFRM)) {
> + rt = dst_rtable(tuplehash->tuple.dst_cache);
> + memset(skb->cb, 0, sizeof(struct inet_skb_parm));
> + IPCB(skb)->iif = skb->dev->ifindex;
> + IPCB(skb)->flags = IPSKB_FORWARDED;
> + return nf_flow_xmit_xfrm(skb, state, &rt->dst);
> + }
> +
> + dir = tuplehash->tuple.dir;
> + flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
> + other_tuple = &flow->tuplehash[!dir].tuple;
> + ip_daddr = other_tuple->src_v4.s_addr;
> + ip6_daddr = &other_tuple->src_v6;
> +
> + if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
> + &ip6_daddr) < 0)
> + return NF_DROP;
> +
> + switch (tuplehash->tuple.xmit_type) {
> + case FLOW_OFFLOAD_XMIT_NEIGH: {
> + struct dst_entry *dst;
> +
> + xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.ifidx);
> + if (!xmit.outdev) {
> + flow_offload_teardown(flow);
> + return NF_DROP;
> + }
> + if (other_tuple->tun.encap_proto == AF_INET6 ||
> + ctx.tun.proto == IPPROTO_IPV6) {
> + struct rt6_info *rt6;
> +
> + rt6 = dst_rt6_info(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw6(rt6->dst.dev,
> + rt6_nexthop(rt6, ip6_daddr));
> + dst = &rt6->dst;
> + } else {
> + rt = dst_rtable(tuplehash->tuple.dst_cache);
> + neigh = ip_neigh_gw4(rt->dst.dev,
> + rt_nexthop(rt, ip_daddr));
> + dst = &rt->dst;
> + }
> + if (IS_ERR(neigh)) {
> + flow_offload_teardown(flow);
> + return NF_DROP;
> + }
> + xmit.dest = neigh->ha;
> + skb_dst_set_noref(skb, dst);
> + break;
> + }
> + case FLOW_OFFLOAD_XMIT_DIRECT:
> + xmit.outdev = dev_get_by_index_rcu(state->net, tuplehash->tuple.out.ifidx);
> + if (!xmit.outdev) {
> + flow_offload_teardown(flow);
> + return NF_DROP;
> + }
> + xmit.dest = tuplehash->tuple.out.h_dest;
> + xmit.source = tuplehash->tuple.out.h_source;
> + break;
> + default:
> + WARN_ON_ONCE(1);
> + return NF_DROP;
> + }
> + xmit.tuple = other_tuple;
> + xmit.needs_gso_segment = tuplehash->tuple.needs_gso_segment;
> +
> + return nf_flow_queue_xmit(state->net, skb, &xmit);
> +}
> +EXPORT_SYMBOL_GPL(nf_flow_offload_ip_hook);
> +
> unsigned int
> nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> const struct nf_hook_state *state)
> @@ -1182,6 +1204,7 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> struct flow_offload *flow;
> struct neighbour *neigh;
> struct rt6_info *rt;
> + __be32 ip_daddr;
> int ret;
>
> tuplehash = nf_flow_offload_ipv6_lookup(&ctx, flow_table, skb);
> @@ -1209,10 +1232,11 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> dir = tuplehash->tuple.dir;
> flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
> other_tuple = &flow->tuplehash[!dir].tuple;
> + ip_daddr = other_tuple->src_v4.s_addr;
> ip6_daddr = &other_tuple->src_v6;
>
> - if (nf_flow_tunnel_v6_push(state->net, skb, other_tuple,
> - &ip6_daddr) < 0)
> + if (nf_flow_tunnel_push(state->net, skb, other_tuple, &ip_daddr,
> + &ip6_daddr) < 0)
> return NF_DROP;
>
> switch (tuplehash->tuple.xmit_type) {
> @@ -1226,10 +1250,8 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
> }
> if (other_tuple->tun.encap_proto == AF_INET ||
> ctx.tun.proto == IPPROTO_IPIP) {
> - __be32 ip_daddr = other_tuple->src_v4.s_addr;
> struct rtable *rt4;
>
> - skb->protocol = htons(ETH_P_IP);
> rt4 = dst_rtable(tuplehash->tuple.dst_cache);
> neigh = ip_neigh_gw4(rt4->dst.dev,
> rt_nexthop(rt4, ip_daddr));
>
> --
> 2.55.0
>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH nf-next v4 6/6] selftests: netfilter: nft_flowtable.sh: Add SIT flowtable selftest
2026-07-03 14:10 [PATCH nf-next v4 0/6] Add IPv4 over IPv6 and SIT flowtable SW acceleration Lorenzo Bianconi
` (4 preceding siblings ...)
2026-07-03 14:10 ` [PATCH nf-next v4 5/6] net: netfilter: Add SIT tunnel flowtable acceleration Lorenzo Bianconi
@ 2026-07-03 14:10 ` Lorenzo Bianconi
5 siblings, 0 replies; 11+ messages in thread
From: Lorenzo Bianconi @ 2026-07-03 14:10 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Felix Fietkau, Matthias Brugger,
AngeloGioacchino Del Regno, Simon Horman, David Ahern,
Ido Schimmel, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
Shuah Khan, Lorenzo Bianconi
Cc: linux-arm-kernel, linux-mediatek, netdev, netfilter-devel,
coreteam, linux-kselftest
Similar to IPIP, IP6IP6 and IPv4 over IPv6, introduce specific selftest
for SIT flowtable sw acceleration in nft_flowtable.sh
Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
---
tools/testing/selftests/net/netfilter/config | 1 +
.../selftests/net/netfilter/nft_flowtable.sh | 45 ++++++++++++++++++++++
2 files changed, 46 insertions(+)
diff --git a/tools/testing/selftests/net/netfilter/config b/tools/testing/selftests/net/netfilter/config
index c3c121b6f300..a212b9edbcd9 100644
--- a/tools/testing/selftests/net/netfilter/config
+++ b/tools/testing/selftests/net/netfilter/config
@@ -35,6 +35,7 @@ CONFIG_IP_SCTP=m
CONFIG_IPV6=y
CONFIG_IPV6_MULTIPLE_TABLES=y
CONFIG_IPV6_TUNNEL=m
+CONFIG_IPV6_SIT=m
CONFIG_IP_VS=m
CONFIG_IP_VS_PROTO_TCP=y
CONFIG_IP_VS_RR=m
diff --git a/tools/testing/selftests/net/netfilter/nft_flowtable.sh b/tools/testing/selftests/net/netfilter/nft_flowtable.sh
index b14c80c6e372..46bd080108be 100755
--- a/tools/testing/selftests/net/netfilter/nft_flowtable.sh
+++ b/tools/testing/selftests/net/netfilter/nft_flowtable.sh
@@ -597,6 +597,10 @@ ip -net "$nsr1" addr add 192.168.210.1/24 dev tun6
ip -net "$nsr1" addr add fee1:3::1/64 dev tun6 nodad
ip netns exec "$nsr1" sysctl net.ipv4.conf.tun6.forwarding=1 > /dev/null
+ip -net "$nsr1" link add name sit1 type sit local 192.168.10.1 remote 192.168.10.2 ttl 255
+ip -net "$nsr1" link set sit1 up
+ip -net "$nsr1" addr add fe01:3::1/64 dev sit1 nodad
+
ip -net "$nsr2" link add name tun0 type ipip local 192.168.10.2 remote 192.168.10.1
ip -net "$nsr2" link set tun0 up
ip -net "$nsr2" addr add 192.168.100.2/24 dev tun0
@@ -608,6 +612,10 @@ ip -net "$nsr2" addr add 192.168.210.2/24 dev tun6
ip -net "$nsr2" addr add fee1:3::2/64 dev tun6 nodad
ip netns exec "$nsr2" sysctl net.ipv4.conf.tun6.forwarding=1 > /dev/null
+ip -net "$nsr2" link add name sit1 type sit local 192.168.10.2 remote 192.168.10.1 ttl 255
+ip -net "$nsr2" link set sit1 up
+ip -net "$nsr2" addr add fe01:3::2/64 dev sit1 nodad
+
ip -net "$nsr1" route change default via 192.168.100.2
ip -net "$nsr2" route change default via 192.168.100.1
@@ -622,6 +630,7 @@ ip -6 -net "$ns2" route add default via dead:2::1
ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif tun0 accept'
ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif tun6 accept'
+ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif sit1 accept'
ip netns exec "$nsr1" nft -a insert rule inet filter forward \
'meta oif "veth0" tcp sport 12345 ct mark set 1 flow add @f1 counter name routed_repl accept'
@@ -648,6 +657,19 @@ if ! test_tcp_forwarding_nat "$ns1" "$ns2" 1 "IP4IP6 tunnel"; then
ret=1
fi
+ip -6 -net "$nsr1" route delete default
+ip -6 -net "$nsr1" route add default via fe01:3::2
+ip -6 -net "$nsr2" route delete default
+ip -6 -net "$nsr2" route add default via fe01:3::1
+
+if test_tcp_forwarding "$ns1" "$ns2" 1 6 "[dead:2::99]" 12345; then
+ echo "PASS: flow offload for ns1/ns2 SIT tunnel"
+else
+ echo "FAIL: flow offload for ns1/ns2 with SIT tunnel" 1>&2
+ ip netns exec "$nsr1" nft list ruleset
+ ret=1
+fi
+
# Create vlan tagged devices for IPIP traffic.
ip -net "$nsr1" link add link veth1 name veth1.10 type vlan id 10
ip -net "$nsr1" link set veth1.10 up
@@ -672,6 +694,11 @@ ip -6 -net "$nsr1" route delete default
ip -6 -net "$nsr1" route add default via fee1:5::2
ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif tun6.10 accept'
+ip -net "$nsr1" link add name sit1.10 type sit local 192.168.20.1 remote 192.168.20.2 ttl 255
+ip -net "$nsr1" link set sit1.10 up
+ip -net "$nsr1" addr add fe01:5::1/64 dev sit1.10 nodad
+ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif sit1.10 accept'
+
ip -net "$nsr2" link add link veth0 name veth0.10 type vlan id 10
ip -net "$nsr2" link set veth0.10 up
ip -net "$nsr2" addr add 192.168.20.2/24 dev veth0.10
@@ -689,6 +716,11 @@ ip -net "$nsr2" link set tun6.10 up
ip -net "$nsr2" addr add 192.168.220.2/24 dev tun6.10
ip -net "$nsr2" addr add fee1:5::2/64 dev tun6.10 nodad
ip netns exec "$nsr2" sysctl net.ipv4.conf.tun6/10.forwarding=1 > /dev/null
+
+ip -net "$nsr2" link add name sit1.10 type sit local 192.168.20.2 remote 192.168.20.1 ttl 255
+ip -net "$nsr2" link set sit1.10 up
+ip -net "$nsr2" addr add fe01:5::2/64 dev sit1.10 nodad
+
ip -6 -net "$nsr2" route delete default
ip -6 -net "$nsr2" route add default via fee1:5::1
@@ -715,6 +747,19 @@ if ! test_tcp_forwarding_nat "$ns1" "$ns2" 1 "IP4IP6 tunnel over vlan"; then
ret=1
fi
+ip -6 -net "$nsr1" route delete default
+ip -6 -net "$nsr1" route add default via fe01:5::2
+ip -6 -net "$nsr2" route delete default
+ip -6 -net "$nsr2" route add default via fe01:5::1
+
+if test_tcp_forwarding "$ns1" "$ns2" 1 6 "[dead:2::99]" 12345; then
+ echo "PASS: flow offload for ns1/ns2 SIT tunnel over vlan"
+else
+ echo "FAIL: flow offload for ns1/ns2 with SIT tunnel over vlan" 1>&2
+ ip netns exec "$nsr1" nft list ruleset
+ ret=1
+fi
+
# Restore the previous configuration
ip -net "$nsr1" route change default via 192.168.10.2
ip -net "$nsr2" route change default via 192.168.10.1
--
2.55.0
^ permalink raw reply related [flat|nested] 11+ messages in thread