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 <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25353
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
This commit is contained in:
Gennaro Cimmino authored and Markus Stockhausen committed 2026-09-29 10:04:39 +02:00
1 parent 3f1de27621
commit 138fabb79f
3 files changed
+36 -4

No files matched your search

@@ -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);
@@ -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 */
@@ -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 */