From 53531e6a644a48c2d5a9423f084ae4e91ec6019a Mon Sep 17 00:00:00 2001 From: Jack Ma Date: Fri, 24 Jul 2026 00:26:16 +0000 Subject: [PATCH 1/3] net: nexthop: add NHA_DST_PORT for fdb nexthops Commit 1274e1cc4226 ("vxlan: ecmp support for mac fdb entries") lets a single inner MAC be reached through a group of remote VTEPs, with the kernel flow-hashing across the group members. Each member carries its own remote IP, but the UDP destination port is always taken from the VXLAN device (vxlan->cfg.dst_port) and cannot be set per member. Some deployments pack several receivers behind one underlay IP and tell them apart by UDP port, so they need a per-nexthop destination port to spread flows across (IP, port) tuples rather than IP alone. Add a netlink attribute NHA_DST_PORT (__be16, mirroring NDA_PORT) that carries an optional UDP destination port on an fdb nexthop. It is only accepted together with NHA_FDB and NHA_GATEWAY; it is stored in struct nh_info and echoed back on dump. The attribute is named generically rather than fdb-specific so it can be reused should another nexthop type ever need a destination port. This patch is control-plane plumbing only; the VXLAN datapath is wired up in a follow-up patch, so behaviour is unchanged for now. Signed-off-by: Jack Ma Reviewed-by: Ido Schimmel Reviewed-by: David Ahern Link: https://patch.msgid.link/20260724-b4-vxlan-fdb-port-v5-1-cd1c6aeee058@gmail.com Signed-off-by: Jakub Kicinski --- include/net/nexthop.h | 2 ++ include/uapi/linux/nexthop.h | 3 +++ net/ipv4/nexthop.c | 20 +++++++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/include/net/nexthop.h b/include/net/nexthop.h index 572e69cda476..7673aaeff3e2 100644 --- a/include/net/nexthop.h +++ b/include/net/nexthop.h @@ -28,6 +28,7 @@ struct nh_config { u8 nh_protocol; u8 nh_blackhole; u8 nh_fdb; + __be16 nh_dst_port; u32 nh_flags; int nh_ifindex; @@ -63,6 +64,7 @@ struct nh_info { u8 family; bool reject_nh; bool fdb_nh; + __be16 dst_port; union { struct fib_nh_common fib_nhc; diff --git a/include/uapi/linux/nexthop.h b/include/uapi/linux/nexthop.h index bc49baf4a267..59cf1cee93bd 100644 --- a/include/uapi/linux/nexthop.h +++ b/include/uapi/linux/nexthop.h @@ -83,6 +83,9 @@ enum { /* u32; read-only; whether any driver collects HW stats */ NHA_HW_STATS_USED, + /* be16; UDP destination port for an fdb nexthop (e.g. VXLAN) */ + NHA_DST_PORT, + __NHA_MAX, }; diff --git a/net/ipv4/nexthop.c b/net/ipv4/nexthop.c index 0f1e21a5c812..af1dcb8ea427 100644 --- a/net/ipv4/nexthop.c +++ b/net/ipv4/nexthop.c @@ -39,6 +39,7 @@ static const struct nla_policy rtm_nh_policy_new[] = { [NHA_ENCAP_TYPE] = { .type = NLA_U16 }, [NHA_ENCAP] = { .type = NLA_NESTED }, [NHA_FDB] = { .type = NLA_FLAG }, + [NHA_DST_PORT] = NLA_POLICY_MIN(NLA_BE16, 1), [NHA_RES_GROUP] = { .type = NLA_NESTED }, [NHA_HW_STATS_ENABLE] = NLA_POLICY_MAX(NLA_U32, true), }; @@ -956,6 +957,9 @@ static int nh_fill_node(struct sk_buff *skb, struct nexthop *nh, } else if (nhi->fdb_nh) { if (nla_put_flag(skb, NHA_FDB)) goto nla_put_failure; + if (nhi->dst_port && + nla_put_be16(skb, NHA_DST_PORT, nhi->dst_port)) + goto nla_put_failure; } else { const struct net_device *dev; @@ -1055,6 +1059,9 @@ static size_t nh_nlmsg_size_single(struct nexthop *nh) break; } + if (nhi->dst_port) + sz += nla_total_size(2); /* NHA_DST_PORT */ + if (nhi->fib_nhc.nhc_lwtstate) { sz += lwtunnel_get_encap_size(nhi->fib_nhc.nhc_lwtstate); sz += nla_total_size(2); /* NHA_ENCAP_TYPE */ @@ -2965,8 +2972,10 @@ static struct nexthop *nexthop_create(struct net *net, struct nh_config *cfg, nhi->family = cfg->nh_family; nhi->fib_nhc.nhc_scope = RT_SCOPE_LINK; - if (cfg->nh_fdb) + if (cfg->nh_fdb) { nhi->fdb_nh = 1; + nhi->dst_port = cfg->nh_dst_port; + } if (cfg->nh_blackhole) { nhi->reject_nh = 1; @@ -3156,6 +3165,15 @@ static int rtm_to_nh_config(struct net *net, struct sk_buff *skb, cfg->nh_fdb = nla_get_flag(tb[NHA_FDB]); } + if (tb[NHA_DST_PORT]) { + if (!tb[NHA_FDB] || !tb[NHA_GATEWAY]) { + NL_SET_ERR_MSG(extack, + "Destination port can only be set on fdb nexthops that have a gateway"); + goto out; + } + cfg->nh_dst_port = nla_get_be16(tb[NHA_DST_PORT]); + } + if (tb[NHA_GROUP]) { if (nhm->nh_family != AF_UNSPEC) { NL_SET_ERR_MSG(extack, "Invalid family for group"); From 951085f82873dc53a62181499a7a7f77b70f9343 Mon Sep 17 00:00:00 2001 From: Jack Ma Date: Fri, 24 Jul 2026 00:26:17 +0000 Subject: [PATCH 2/3] vxlan: honor per-nexthop fdb destination port When an fdb entry points at a nexthop group, vxlan_fdb_nh_path_select() resolves the selected leg's remote IP but leaves the UDP destination port at the device default (vxlan->cfg.dst_port). Extend nexthop_path_fdb_result() to also return the selected nexthop's NHA_DST_PORT (0 when unset) and have vxlan_fdb_nh_path_select() store it in rdst->remote_port. vxlan_xmit_one() already prefers rdst->remote_port when non-zero and falls back to the device port otherwise, so nexthops without a port are unaffected. This lets one fdb nexthop group load-balance a flow across legs that share an underlay IP but differ in UDP destination port. Signed-off-by: Jack Ma Reviewed-by: Ido Schimmel Reviewed-by: David Ahern Link: https://patch.msgid.link/20260724-b4-vxlan-fdb-port-v5-2-cd1c6aeee058@gmail.com Signed-off-by: Jakub Kicinski --- include/net/nexthop.h | 4 +++- include/net/vxlan.h | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/include/net/nexthop.h b/include/net/nexthop.h index 7673aaeff3e2..f86c115074d7 100644 --- a/include/net/nexthop.h +++ b/include/net/nexthop.h @@ -576,7 +576,8 @@ struct fib_nh_common *nexthop_fdb_nhc(struct nexthop *nh) } static inline struct fib_nh_common *nexthop_path_fdb_result(struct nexthop *nh, - int hash) + int hash, + __be16 *dst_port) { struct nh_info *nhi; struct nexthop *nhp; @@ -585,6 +586,7 @@ static inline struct fib_nh_common *nexthop_path_fdb_result(struct nexthop *nh, if (unlikely(!nhp)) return NULL; nhi = rcu_dereference(nhp->nh_info); + *dst_port = nhi->dst_port; return &nhi->fib_nhc; } #endif diff --git a/include/net/vxlan.h b/include/net/vxlan.h index dfba89695efc..6e64757151b8 100644 --- a/include/net/vxlan.h +++ b/include/net/vxlan.h @@ -567,8 +567,9 @@ static inline bool vxlan_fdb_nh_path_select(struct nexthop *nh, struct vxlan_rdst *rdst) { struct fib_nh_common *nhc; + __be16 dst_port = 0; - nhc = nexthop_path_fdb_result(nh, hash >> 1); + nhc = nexthop_path_fdb_result(nh, hash >> 1, &dst_port); if (unlikely(!nhc)) return false; @@ -583,6 +584,8 @@ static inline bool vxlan_fdb_nh_path_select(struct nexthop *nh, break; } + rdst->remote_port = dst_port; + return true; } From 992965451db727a15988931655a825c5e0312edb Mon Sep 17 00:00:00 2001 From: Jack Ma Date: Fri, 24 Jul 2026 00:26:18 +0000 Subject: [PATCH 3/3] selftests: net: add coverage for fdb nexthop dst_port Add coverage for the new per-nexthop VXLAN destination port (NHA_DST_PORT). In fib_nexthops.sh, new ipv4_fdb_port_fcnal() and ipv6_fdb_port_fcnal() tests check that a dst_port is accepted on an fdb nexthop that has a gateway and echoed back on dump, that it is rejected without a gateway and rejected when zero, that a group may hold legs that differ only in UDP port, and that a portless fdb nexthop omits the attribute. The tests SKIP when iproute2 lacks the "dst_port" keyword. In test_vxlan_nh.sh, basic_tx_common() gains a second fdb nexthop group whose nexthop carries a destination port that differs from the VXLAN device default, plus a flower filter keyed on that port, to confirm the per-nexthop port is used on the wire. The test now requires an iproute2 with dst_port support. Signed-off-by: Jack Ma Reviewed-by: Ido Schimmel Reviewed-by: David Ahern Link: https://patch.msgid.link/20260724-b4-vxlan-fdb-port-v5-3-cd1c6aeee058@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/net/fib_nexthops.sh | 83 ++++++++++++++++++++ tools/testing/selftests/net/test_vxlan_nh.sh | 40 +++++++++- 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/tools/testing/selftests/net/fib_nexthops.sh b/tools/testing/selftests/net/fib_nexthops.sh index ac868a731694..3d347126730a 100755 --- a/tools/testing/selftests/net/fib_nexthops.sh +++ b/tools/testing/selftests/net/fib_nexthops.sh @@ -30,6 +30,7 @@ IPV4_TESTS=" ipv4_large_res_grp ipv4_compat_mode ipv4_fdb_grp_fcnal + ipv4_fdb_port_fcnal ipv4_mpath_select ipv4_torture ipv4_res_torture @@ -44,6 +45,7 @@ IPV6_TESTS=" ipv6_large_res_grp ipv6_compat_mode ipv6_fdb_grp_fcnal + ipv6_fdb_port_fcnal ipv6_mpath_select ipv6_torture ipv6_res_torture @@ -432,6 +434,15 @@ check_nexthop_fdb_support() fi } +check_nexthop_fdb_port_support() +{ + $IP nexthop help 2>&1 | grep -q "dst_port" + if [ $? -ne 0 ]; then + echo "SKIP: iproute2 too old, missing nexthop dst_port support" + return $ksft_skip + fi +} + check_nexthop_res_support() { $IP nexthop help 2>&1 | grep -q resilient @@ -541,6 +552,42 @@ ipv6_fdb_grp_fcnal() $IP link del dev vx10 } +ipv6_fdb_port_fcnal() +{ + echo + echo "IPv6 fdb nexthop dst_port functional" + echo "------------------------------------" + + check_nexthop_fdb_port_support + if [ $? -eq $ksft_skip ]; then + return $ksft_skip + fi + + # NHA_DST_PORT: optional per-nexthop VXLAN destination UDP port, + # letting an fdb nexthop group balance a flow across legs that share + # an underlay IP but listen on different UDP ports. + run_cmd "$IP nexthop add id 80 via 2001:db8:91::2 fdb dst_port 4790" + check_nexthop "id 80" \ + "id 80 via 2001:db8:91::2 scope link fdb dst_port 4790" + log_test $? 0 "Fdb nexthop with dst_port" + + run_cmd "$IP nexthop add id 81 fdb dst_port 4790" + log_test $? 2 "Fdb nexthop with dst_port but no gateway" + + run_cmd "$IP nexthop add id 81 via 2001:db8:91::2 fdb dst_port 0" + log_test $? 2 "Fdb nexthop with dst_port 0" + + run_cmd "$IP nexthop add id 82 via 2001:db8:91::2 fdb dst_port 4789" + run_cmd "$IP nexthop add id 83 via 2001:db8:91::3 fdb dst_port 5789" + run_cmd "$IP nexthop add id 106 group 82/83 fdb" + check_nexthop "id 106" "id 106 group 82/83 fdb" + log_test $? 0 "Fdb nexthop group with legs differing in dst_port" + + run_cmd "$IP nexthop add id 84 via 2001:db8:91::2 fdb" + check_nexthop "id 84" "id 84 via 2001:db8:91::2 scope link fdb" + log_test $? 0 "Fdb nexthop without dst_port omits dst_port" +} + ipv4_fdb_grp_fcnal() { local rc @@ -641,6 +688,42 @@ ipv4_fdb_grp_fcnal() $IP link del dev vx10 } +ipv4_fdb_port_fcnal() +{ + echo + echo "IPv4 fdb nexthop dst_port functional" + echo "------------------------------------" + + check_nexthop_fdb_port_support + if [ $? -eq $ksft_skip ]; then + return $ksft_skip + fi + + # NHA_DST_PORT: optional per-nexthop VXLAN destination UDP port, + # letting an fdb nexthop group balance a flow across legs that share + # an underlay IP but listen on different UDP ports. + run_cmd "$IP nexthop add id 30 via 172.16.1.2 fdb dst_port 4790" + check_nexthop "id 30" \ + "id 30 via 172.16.1.2 scope link fdb dst_port 4790" + log_test $? 0 "Fdb nexthop with dst_port" + + run_cmd "$IP nexthop add id 31 fdb dst_port 4790" + log_test $? 2 "Fdb nexthop with dst_port but no gateway" + + run_cmd "$IP nexthop add id 31 via 172.16.1.2 fdb dst_port 0" + log_test $? 2 "Fdb nexthop with dst_port 0" + + run_cmd "$IP nexthop add id 32 via 172.16.1.2 fdb dst_port 4789" + run_cmd "$IP nexthop add id 33 via 172.16.1.3 fdb dst_port 5789" + run_cmd "$IP nexthop add id 105 group 32/33 fdb" + check_nexthop "id 105" "id 105 group 32/33 fdb" + log_test $? 0 "Fdb nexthop group with legs differing in dst_port" + + run_cmd "$IP nexthop add id 34 via 172.16.1.2 fdb" + check_nexthop "id 34" "id 34 via 172.16.1.2 scope link fdb" + log_test $? 0 "Fdb nexthop without dst_port omits dst_port" +} + ipv4_mpath_select() { local rc dev match h addr diff --git a/tools/testing/selftests/net/test_vxlan_nh.sh b/tools/testing/selftests/net/test_vxlan_nh.sh index 20f3369f776b..5ce6f27f6cf4 100755 --- a/tools/testing/selftests/net/test_vxlan_nh.sh +++ b/tools/testing/selftests/net/test_vxlan_nh.sh @@ -56,6 +56,17 @@ tc_stats_get() tc_rule_handle_stats_get "dev dummy1 egress" 101 ".packets" "-n $ns1" } +nh_stats_get_port() +{ + ip -n "$ns1" -s -j nexthop show id 20 | \ + jq ".[][\"group_stats\"][][\"packets\"]" +} + +tc_stats_get_port() +{ + tc_rule_handle_stats_get "dev dummy1 egress" 102 ".packets" "-n $ns1" +} + basic_tx_common() { local af_str=$1; shift @@ -90,6 +101,31 @@ basic_tx_common() busywait "$BUSYWAIT_TIMEOUT" until_counter_is "== 1" tc_stats_get > /dev/null check_err $? "tc filter stats did not increase" + # Add a second FDB nexthop group whose nexthop carries a per-nexthop + # destination port (NHA_DST_PORT) that differs from the VXLAN device + # default. Matching outer traffic must egress with that port, so a + # separate flower filter keyed on the new port catches it. + run_cmd "tc -n $ns1 filter add dev dummy1 egress proto $proto \ + pref 1 handle 102 flower ip_proto udp dst_ip $remote_addr \ + dst_port 4790 action pass" + + run_cmd "ip -n $ns1 nexthop add id 2 via $remote_addr fdb dst_port 4790" + run_cmd "ip -n $ns1 nexthop add id 20 group 2 fdb" + + run_cmd "bridge -n $ns1 fdb add 00:11:22:33:44:66 dev vx0 \ + self static nhid 20" + + run_cmd "ip netns exec $ns1 mausezahn vx0 -a own \ + -b 00:11:22:33:44:66 -c 1 -q" + + busywait "$BUSYWAIT_TIMEOUT" until_counter_is "== 1" \ + nh_stats_get_port > /dev/null + check_err $? "FDB nexthop group stats did not increase (with port)" + + busywait "$BUSYWAIT_TIMEOUT" until_counter_is "== 1" \ + tc_stats_get_port > /dev/null + check_err $? "tc filter stats did not increase (with port)" + log_test "VXLAN FDB nexthop: $af_str basic Tx" } @@ -210,8 +246,8 @@ require_command arping require_command ndisc6 require_command jq -if ! ip nexthop help 2>&1 | grep -q "stats"; then - echo "SKIP: iproute2 ip too old, missing nexthop stats support" +if ! ip nexthop help 2>&1 | grep -q "dst_port"; then + echo "SKIP: iproute2 ip too old, missing nexthop dst_port support" exit "$ksft_skip" fi