From 138fabb79f8d68d65e40c8b8d0c0494b19053d0f Mon Sep 17 00:00:00 2001 From: Gennaro Cimmino Date: Tue, 22 Sep 2026 21:26:31 +0200 Subject: [PATCH] realtek: l3: do not invent an L2 entry for a next hop When a route is programmed while the switch holds no L2 entry for the gateway, rtldsa_l2_nexthop_add() invents one carrying the don't-care port priv->r->port_ignore. Observed on an RTL9303: with such a next hop the destination receives every forwarded frame twice, once from the switch and once from the Linux stack. Where the route entry can trap, do not invent the row, and trap the route to the CPU instead, which is what otto_l3_fib_add_v4() already does for a host route with no gateway. The invented row is written static, so it does not age, and the software bridge offers no replacement: dsa_user_fdb_event() drops an address learned on an offloaded port before the driver sees it. Leaving the slot to the address lets the switch learn it from the traffic the CPU routes, and a later neighbour update programs a real port. From the code, not tested: a gateway held as a permanent or NOARP neighbour produces no such update, so its route stays in software. A route that only claims entries the switch learned never clears the route count of the entry it claims: that count may still hold routes forwarding through the entry. Tested on an RTL9303 (Hasivo S1100W-8XGT-SE) against pristine main, same board and same rig (main at 19ff245b92; this patch on c09e3f28ba, which only adds an SPI backport): two hosts on one bridge port, one untagged in VLAN 1 and one tagged in VLAN 3, a /32 route to each, programmed after 340 s of neighbour silence so the gateways' L2 entries have aged out. On main both get a static next hop with the don't-care port, and five pings collect twelve duplicates: tcpdump at the destination shows each echo request twice with the same IP id, once at ttl 64 and once at ttl 63, and the entry is still static afterwards. With this patch both routes are trapped and the five pings cross in software without loss or duplicates, each request once at ttl 63. A trapped route stays trapped while nothing crosses it (ten readings 30 s apart); the pings let the switch learn the address, and after them the route is back in hardware on a learned, dynamic entry with the real port. Programmed while the neighbours are fresh, both kernels behave the same: a real port and no duplicates. A /24 through the same gateway, which lands in the prefix table rather than the host one, is trapped the same way, and its five pings cross at ttl 63 without duplicates. Not changed here, and the same on both kernels: an entry that ages while a route already forwards through it keeps its next-hop bit with the don't-care port, so after 340 s of silence the first of five pings drew one duplicate before the switch learned the address again. RTL838x and RTL931x never reach this code: their otto_l3_config carries no .setup, and both notifiers return early without one, as does an RTL930x built without CONFIG_NET_DSA_RTL83XX_RTL930X_L3_OFFLOAD. RTL839x reaches it, but routes through a PIE rule rather than a route entry with an action, and what its switch does with a route rule that no longer forwards is not something I could find in the SDK or test. The change is therefore keyed on use_l3_tables: RTL839x still gets the invented entry and a PIE rule forwarding through it, exactly as before. Whether it shows the duplicates too was not tested. Assisted-by: Claude:claude-opus-5 Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Gennaro Cimmino Link: https://github.com/openwrt/openwrt/pull/25353 Signed-off-by: Markus Stockhausen --- .../drivers/net/dsa/rtl83xx/common.c | 18 +++++++++++++++++- .../files-6.18/drivers/net/dsa/rtl83xx/l3.c | 19 +++++++++++++++++-- .../drivers/net/dsa/rtl83xx/rtl-otto.h | 3 ++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/common.c b/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/common.c index 662ee6a9546..81dfa43d750 100644 --- a/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/common.c +++ b/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/common.c @@ -416,7 +416,8 @@ static void rtldsa_l2_uc_put_row(struct rtl838x_switch_priv *priv, * Called from the L3 layer * The index in the L2 hash table is filled into nh->l2_id; */ -int rtldsa_l2_nexthop_add(struct rtl838x_switch_priv *priv, struct otto_l3_nexthop *nh) +int rtldsa_l2_nexthop_add(struct rtl838x_switch_priv *priv, struct otto_l3_nexthop *nh, + bool require_existing) { struct rtl838x_l2_entry e = {}; u64 seed = priv->r->l2_hash_seed(nh->mac, nh->rvid); @@ -436,6 +437,21 @@ int rtldsa_l2_nexthop_add(struct rtl838x_switch_priv *priv, struct otto_l3_nexth return -1; } + /* With a next hop that has no port, an RTL930x delivers every forwarded + * frame twice, once from the switch and once from the Linux stack. A + * row invented here would be static, and nothing would replace it: + * dsa_user_fdb_event() drops an address the software bridge learns on + * an offloaded port before the driver sees it. A caller that can trap + * the route leaves the slot to the address, and lets the switch learn + * it. + */ + if (require_existing && !e.valid) { + if (nh->l2_installed) + rtldsa_l2_uc_put_row(priv, nh); + nh->l2_installed = false; + return -ENOENT; + } + /* Found an existing (e->valid is true) or empty entry, make it a nexthop entry */ if (nh->l2_installed && nh->l2_id != idx) rtldsa_l2_uc_put_row(priv, nh); diff --git a/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/l3.c b/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/l3.c index ed91c1ada5b..1aa79421999 100644 --- a/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/l3.c +++ b/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/l3.c @@ -990,6 +990,7 @@ static void otto_l3_route_compact(struct otto_l3_ctrl *ctrl, struct otto_l3_rout static int otto_l3_nexthop_update(struct otto_l3_ctrl *ctrl, __be32 ip_addr, u64 mac) { struct rtl838x_switch_priv *priv = ctrl->priv; + bool require_existing = ctrl->cfg->use_l3_tables; struct otto_l3_route *r; bool known; @@ -1009,6 +1010,8 @@ static int otto_l3_nexthop_update(struct otto_l3_ctrl *ctrl, __be32 ip_addr, u64 return -ENOENT; list_for_each_entry(r, &ctrl->routes_list, list) { + bool no_port; + if (r->gw_ip != ip_addr) continue; @@ -1027,11 +1030,23 @@ static int otto_l3_nexthop_update(struct otto_l3_ctrl *ctrl, __be32 ip_addr, u64 ctrl->cfg->set_egress_mac(ctrl, r->id, mac); /* Update ROUTING table: map gateway-mac and switch-mac id to route id */ - if (!rtldsa_l2_nexthop_add(priv, &r->nh)) + if (!rtldsa_l2_nexthop_add(priv, &r->nh, require_existing)) r->nh.l2_installed = true; + /* A next hop with no port delivers the frame twice, so where the + * route entry can trap, let the CPU route it alone until an update + * brings one, the way otto_l3_fib_add_v4() treats a host route + * with no gateway. A family that routes through a PIE rule keeps + * the rule it had. + */ + no_port = ctrl->cfg->use_l3_tables && + r->nh.port == priv->r->port_ignore; + if (no_port && r->attr.action != ROUTE_ACT_TRAP2CPU) + dev_info(ctrl->dev, "no port for %pI4, routing %pI4/%d in software\n", + &ip_addr, &r->dst_ip, r->prefix_len); + r->attr.valid = true; - r->attr.action = ROUTE_ACT_FORWARD; + r->attr.action = no_port ? ROUTE_ACT_TRAP2CPU : ROUTE_ACT_FORWARD; r->attr.type = ROUTE_TYPE_IP4UC; r->attr.hit = false; /* Reset route-used indicator */ diff --git a/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/rtl-otto.h b/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/rtl-otto.h index 058a73fbf04..a012e367e5a 100644 --- a/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/rtl-otto.h +++ b/target/linux/realtek/files-6.18/drivers/net/dsa/rtl83xx/rtl-otto.h @@ -1409,7 +1409,8 @@ static inline struct rtldsa_l2_uc *rtldsa_l2_uc_lookup(struct rtl838x_switch_pri return &priv->l2_uc_map[idx]; } -int rtldsa_l2_nexthop_add(struct rtl838x_switch_priv *priv, struct otto_l3_nexthop *nh); +int rtldsa_l2_nexthop_add(struct rtl838x_switch_priv *priv, struct otto_l3_nexthop *nh, + bool require_existing); int rtldsa_l2_nexthop_del(struct rtl838x_switch_priv *priv, struct otto_l3_nexthop *nh); #endif /* _RTL838X_H */