Commit Graph
2531 Commits
Author SHA1 Message Date
Gennaro Cimmino f74545c881 realtek: l3: let each family map its IPv6 rows
The prefix row placement is common to RTL839x and RTL930x, but where an
IPv6 entry sits is not: an RTL839x PIE rule matches a whole IPv6
destination in any row, while an RTL930x entry spans three rows and may
only start on two rows of every eight. otto_l3_v6_row() and
otto_l3_v6_slot() chose between the two layouts through use_l3_tables,
which says how a family routes, not how its table is laid out.

Give the families v6_slot_to_row() and v6_row_to_slot() and move each
layout next to its row mover. Ordering, insertion, compaction and the
capacity and IPv4/IPv6 collision checks stay common, and keep calling
the same two helpers, which now ask the family. The common code calls
them only for a family with a row mover.

The formulas move unchanged. Built on the host from l3.c as it is, the
placement code before and after this change maps every slot and row of
both layouts alike, and runs 160000 random IPv4 and IPv6 insertions and
removals, past a full table on both families, with the same rows and
the same moves; a one-line mutation planted in each of the four
callbacks makes the results differ. On an HPE 1920-48G (RTL8393) an
IPv6 test round passes 20 of 20 checks, among them a /48 and a /64
inserted in both orders and IPv4 and IPv6 routes side by side, the
routes of those checks forwarded in hardware to the right gateway.
RTL930x is compile-tested only.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25868
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-13 13:36:18 +02:00
Quinn O'Shea 3fba6794e2 realtek: dsa: count every route an L2 entry can carry
The use map counts the routes that forward through an L2 unicast entry
in seven bits. At 127 the count stops, and an entry that got there is
never released: when its routes are gone it keeps its next hop flag
until the switch restarts. RTL930x takes 512 prefix and 1536 host
routes, so more than 126 through one gateway is an ordinary table.

Give the count fifteen bits, more than the routes of any family. The
map grows from one to two bytes per entry.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Quinn O'Shea <qoshea@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/25859
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-13 10:23:49 +02:00
Gennaro Cimmino d081677551 realtek: l3: ignore neighbours at the unspecified address
A route with no gateway keeps the unspecified address where its gateway
would be, 0.0.0.0 or ::, and so does the route of each address of the
switch. Their rows trap to the CPU. The kernel accepts a neighbour at
that address when one is added by hand, and otto_l3_nexthop_update(),
which matches a neighbour on the gateway and the device recorded for
each route, takes it for the gateway of such routes and can turn their
rows into forwarding ones. Leave such a neighbour alone: it is the
gateway of no route.

HPE 1920-48G (RTL8393), host B on VLAN 3, 10.99.0.0/16 dev switch.3,
and

  ip neigh add 0.0.0.0 lladdr <MAC of host B> dev switch.3 nud permanent

Without the change the row of 10.99.0.0/16 forwarded 8 of 8 echo
requests from another VLAN to host B with TTL 63, none reached the CPU,
and the rows of the switch's addresses and on-link prefixes on switch.3
turned to forwarding as well: a host on another VLAN got no answer from
10.91.3.1, 0 of 3. With :: on IPv6 the trap rows of fd00:90:3::1/128,
fd00:90:3::/64 and 2001:db8:99::/48 went through the same update and
took a packet counter each, but kept trapping here.

With the change the neighbour event still arrives and changes nothing:
the rows keep trapping and take no counter, the 8 echo requests reach
the CPU, and the switch answers on 10.91.3.1 and fd00:90:3::1, 3 of 3.
A route through a gateway, measured before the neighbour was added,
forwards in hardware on both, 8 of 8 with TTL and hop limit 63.
RTL930x is compile-tested only; families without an L3 setup never
queue the neighbour event.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25863
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-13 10:22:23 +02:00
Gennaro Cimmino 85332a8c6e realtek: l3: add get_dmac()
The RTL839x route dump reads a ROUTING row back with
otto_l3_839x_nexthop_read(), which fills a whole route to hand over a
gateway MAC and a switch MAC entry. Read DMAC entries back the way they
are written instead: get_dmac() fills the struct otto_l3_dmac that
set_dmac() takes. RTL839x decodes the gateway MAC and SW_MAC_IDX from
the row; RTL930x reads the MAC from its egress MAC table and leaves
smac_id at 0, since its egress interfaces name the source MAC. A read
that fails returns its error, and the dump prints "-" for it.

Only the RTL839x dump uses it; nothing else changes. On an HPE 1920-48G
(RTL8393) it reads back the gateway MAC with MAC_ID 1 for a route out of
a device with a MAC of its own, and 0 for one out of the switch MAC.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25852
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-13 08:02:59 +02:00
Gennaro Cimmino d795c6a464 realtek: l3: add get_smac() and set_smac()
Common code reaches the source MACs of the two families in two ways:
RTL930x callers add L3_EGRESS_DMACS to an index of its egress MAC
table, which holds destination and source MACs alike, while RTL839x
writes its switch MAC entries through set_switch_mac(). Give both the
same pair, get_smac() and set_smac(), on the index of the source MAC
entry: RTL839x reads and writes ROUTING_SA_CTRL, RTL930x adds
L3_EGRESS_DMACS itself, so the layout of its table stays in the RTL930x
code.

The accessors report a failed table access, and the callers act on it
where they can. An egress interface whose source MAC cannot be written
is not taken, and its route is not offloaded; nor is a switch MAC entry
that cannot be written, and its route keeps to the CPU. The allocator
skips a source MAC it cannot read: it is neither free nor a match. An
interface that cannot take its device's new MAC keeps the old one, as
when none is free. A source MAC that cannot be cleared when its last
route goes, or at RTL930x setup, is logged; a failed setup would fail
the whole switch probe. The RTL930x next hop dump prints "-" for one it
cannot read. Before, all of these were ignored. The egress interface
allocator returns -ENOSPC rather than -1 when none is free, as
otto_l3_dmac_get() and otto_l3_switch_mac_get() do.

RTL839x writes registers, which cannot fail, so it behaves as before.
Tested on an HPE 1920-48G (RTL8393): a VLAN device with a MAC of its
own takes switch MAC entry 1, frames to it are routed in hardware, and
the entry is freed when the routes holding it go. RTL930x
compile-tested only.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25852
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-13 08:02:59 +02:00
Gennaro Cimmino 1ee96a2f23 realtek: l3: add a capability for per-route source MACs
The common L3 code tells the RTL839x source MAC model apart by whether
the family sets set_switch_mac: there a route takes the source MAC
entry of the device it goes out of, its DMAC entry names that entry,
and the entries are also the MACs whose frames are routed. Once both
families reach their source MACs through the same operations, that
test no longer tells them apart.

Name the model with smac_per_route, set for RTL839x, and test that
wherever the code chose it by set_switch_mac. No functional change:
RTL839x sets both, RTL838x, RTL930x and RTL931x neither.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25852
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-13 08:02:59 +02:00
Quinn O'Shea 02a3d72c96 realtek: dsa: police a port only for the flower rule that asks for it
A flower rule whose only action is a byte rate police is programmed as
the rate limiter of the port without a look at what the rule matches. A
rule that polices broadcast frames to 10 Mbit/s limits all traffic of
the port to 10 Mbit/s.

Deleting has the matching defect: the delete callback also arrives for
rules that never made it into the hardware, and any of them that is not
a PIE rule switches the limiter of the port off, while the rule that
set the limiter up is still reported as offloaded.

Program the limiter only for a rule that reaches every frame of the
port: in chain 0, for all protocols and without match. Remember which
rule that was and remove the limiter only when this rule goes. Any
other police rule is no longer offloaded.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Quinn O'Shea <qoshea@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/25845
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-12 22:10:50 +02:00
Jonas Jelonek fa33fb66fc realtek: pcs: poll in-band SGMII links
Dynamic SGMII PHY links may remain software-forced, which prevents PCS
state changes from generating MAC link interrupts. Enable phylink PCS
polling for in-band SGMII so links recover after the PCS settles
following a mode change.

Link: https://github.com/openwrt/openwrt/pull/25118
Signed-off-by: Jonas Jelonek <jonas@jonasjelonek.de>
2026-10-12 20:06:56 +02:00
Jonas Jelonek 9cf511cfb4 realtek: dsa: support RTL931x MAC force mode
RTL931x controls forced link, speed, duplex, and flow control with
separate enable bits. Describe the register layout and provide a MAC
link-state source selector so the common RTL93xx callbacks can manage
its force-mode transitions.

This supports fixed links, CPU ports, and dynamic SFP PHYs while
leaving statically described PHY links and direct PCS links
hardware-controlled. Dynamic SGMII and out-of-band PHYs remain forced,
while dynamic USXGMII PHYs return to PCS-controlled state after the
force pulse.

Assisted-by: ChatGPT (OpenAI GPT-5.6 Sol)
Link: https://github.com/openwrt/openwrt/pull/25118
Signed-off-by: Jonas Jelonek <jonas@jonasjelonek.de>
2026-10-12 20:06:56 +02:00
Jonas Jelonek 951eb211c7 realtek: dsa: split MAC link-down callbacks
Use separate RTL83xx and RTL93xx link-down callbacks so their
family-specific force-mode handling can evolve independently.

The RTL83xx callback retains the legacy force-mode clearing behavior.
The RTL93xx callback preserves force mode for forced RTL930x links and
keeps PCS source selection with the corresponding link-down handling.
This also prepares the common RTL93xx path for RTL931x support.

Assisted-by: ChatGPT (OpenAI GPT-5.6 Sol)
Link: https://github.com/openwrt/openwrt/pull/25118
Signed-off-by: Jonas Jelonek <jonas@jonasjelonek.de>
2026-10-12 20:06:56 +02:00
Jonas Jelonek 6d790a1ccb realtek: dsa: describe MAC force mode fields
Move the family-specific MAC force mode register layout into the switch
configuration. This removes family checks from the RTL83xx link-up path
and converts RTL930x handling to the same field descriptions.

Describe link-state and flow-control force enables separately along with
the fields used to construct resolved MAC state. This allows phylink's
resolved pause settings to remain forced when link state is controlled by
hardware. Add the initial RTL931x link-force description in preparation
for later RTL931x support.

Assisted-by: ChatGPT (OpenAI GPT-5.6 Sol)
Link: https://github.com/openwrt/openwrt/pull/25118
Signed-off-by: Jonas Jelonek <jonas@jonasjelonek.de>
2026-10-12 20:06:56 +02:00
Jonas Jelonek 76484bf8b8 realtek: dsa: rtl930x: handle SFP PHY link state
RTL930x SFP ports may use either a directly attached SerDes module
or a module containing a PHY discovered after switch setup. Static
PHY polling cannot provide link state for the latter, while forcing
every port would override hardware state for PCS and statically
polled PHY links.

Program the MAC link-state source for non-static links. Dynamic SGMII
and out-of-band PHYs select their PHY state source and remain forced.
Dynamic USXGMII PHYs pulse force with the resolved parameters before
returning to PCS-controlled state. Fixed links and CPU ports also
remain forced.

Keep flow-control forcing independent from link-state forcing so
phylink's resolved pause settings remain active when link state returns
to hardware control.

Preserve force mode while a forced link is down so the MAC reports
the down state instead of reverting to its hardware source.

Tested on RTL930x hardware.

Assisted-by: ChatGPT (OpenAI GPT-5.6 Sol)
Link: https://github.com/openwrt/openwrt/pull/25118
Signed-off-by: Jonas Jelonek <jonas@jonasjelonek.de>
2026-10-12 20:06:56 +02:00
Jonas Jelonek 370eac4f6e realtek: dsa: use regmap in phylink MAC callbacks
Convert switch register accesses in the phylink MAC configuration and
link callbacks to the shared regmap. Use regmap_update_bits() for masked
updates and regmap_read()/regmap_write() when assembling full force-mode
values.

The callbacks are void, so leave regmap return values unchecked to match
the existing callback behavior.

Assisted-by: ChatGPT (OpenAI GPT-5.6 Sol)
Link: https://github.com/openwrt/openwrt/pull/25118
Signed-off-by: Jonas Jelonek <jonas@jonasjelonek.de>
2026-10-12 20:06:55 +02:00
Jonas Jelonek 6b7e1ee1df realtek: dsa: obtain switch regmap during probe
The switch register range is exposed by the device tree node as a syscon.
Obtain its regmap while probing and retain it in the private data so
register accesses can be converted to the regmap API. Return the
underlying error if the regmap cannot be obtained.

Assisted-by: ChatGPT (OpenAI GPT-5.6 Sol)
Link: https://github.com/openwrt/openwrt/pull/25118
Signed-off-by: Jonas Jelonek <jonas@jonasjelonek.de>
2026-10-12 20:06:55 +02:00
Gennaro Cimmino 64fdfbae91 realtek: l3: rtl839x: offload IPv6 routes
RTL839x offloads IPv4 routes only: IPv6 FIB, policy rule and neighbour
events are dropped where the L3 tables are not used (#25768 item 1).
Now that an L3 rule can match an IPv6 destination, take them: an IPv6
route gets a PIE rule matching its prefix and FRAME_TYPE IPv6, which
forwards to its gateway's L2 entry or traps, as an IPv4 one does.

IPv6 rules fill the L3 range from the last rule down and IPv4 ones from
rule 1 up, the longest prefix on the lowest rule in each, as on
RTL930x; FRAME_TYPE keeps the two apart, so either family can use the
whole range. An RTL839x rule holds a whole IPv6 destination and can
sit anywhere, where an RTL930x entry takes three rows in fixed places,
so the row helpers tell the two apart by use_l3_tables.

As on IPv4, an IPv6 route through a gateway takes the switch MAC
entry of the device it goes out of, and an address of the switch or an
on-link prefix that of its device: the trap rules match frames to the
switch's MACs, and a device with a MAC of its own and no IPv4 may have
no other route to take one. After a device's MAC change its IPv6
routes through a gateway are resolved again, through ndisc. A routed
IPv6 packet whose hop limit runs out goes to the CPU, which answers
it: IP6_HL_EXCEED, bits 9:8 of ROUTING_EXCPT_CTRL, set to 2, trap
(Realtek GPL SDK, rtk_cypress_regField_list.c for the field,
dal_cypress_trap.c for 0 drop, 1 forward, 2 trap). debugfs prints IPv6
rules.

Measured on an HPE 1920-48G (RTL8393), main 404823678f with this
series, 8 echo requests from VLAN 7 per case, forwarding off,
2001:db8:99::/48 via a gateway in VLAN 3 behind another port:
- blocks 0-3 select templates 1 and 3; the switch's IPv6 addresses,
  on-link prefixes and ULA /48 trap from rule 511 down
- routed in hardware: 8/8 at the gateway, hop limit 63, source the
  switch MAC, none at the CPU, the rule's counter +8
- with 2001:db8:99:5::/64 via a second gateway, added before or after:
  :5:: to the second and :6:: to the first, 8/8 each, none at the CPU
- hop limit 1, 4 requests, IPv6 forwarding on: 4/4 to the CPU, 4 Time
  Exceeded back. With bits 9:8 set to 1 instead the 4 reached the
  gateway with hop limit 0, with 0 they reached nothing.
- gateway down: neighbour failed, the route trapped; back: forwarding
  in hardware again
- an IPv4 route installed alongside: 8/8 each in hardware
- VLAN 7's device IPv6-only with a MAC of its own: that MAC took a
  switch MAC entry and the 8 sent to it were routed in hardware, none
  at the CPU; before addresses and on-link prefixes took one, it got
  none and all 8 went to the CPU
- 2001:db8:99:6::/64 via the first gateway, then 140 more /64s and
  the /48 via the second: the first /64 went down to rule 369, in
  block 2, and took 8/8 there; :8:: went to the second through the
  /48 on rule 511; with the 140 gone the /64 was back on rule 509,
  8/8
- IPv4, 10.99.0.0/16 via one gateway and 10.99.5.0/24 via another,
  added in either order: 10.99.5 to the /24's, 10.99.6 to the /16's,
  8/8 each, none at the CPU
- IPv4, 10.99.0.0/16 via one gateway and 10.0.0.0/8 via another,
  then 390 /32s via the other: the /16 went up to rule 399, in block 3,
  and took 8/8 there, none at the CPU; with the 390 gone it was back
  on rule 9

RTL930x: no change. Its IPv6 path took these events already, its row
helpers return what they did, and the rest depends on set_switch_mac,
on the RTL839x setup, writer or debugfs dump. RTL838x and RTL931x have
no setup, so their FIB and neighbour events are still ignored.
Compile-tested on rtl930x, with IPv6 and without.

Not tested: an IPv6 route out of a device with a MAC of its own, a MAC
change under IPv6 routes, an IPv6 policy rule over offloaded routes,
IPv6 rules in blocks 0 and 1, IPv4 and IPv6 rules meeting in a full
range, bridged IPv6 frames whose destination falls in an offloaded
prefix.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25838
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-12 18:54:45 +02:00
Gennaro Cimmino b086ad2f28 realtek: pie: rtl839x: let L3 rules match IPv6 destinations
Since b98061ac6e ("realtek: pie: rtl839x: keep the first 512 rules
for L3") blocks 0-3 hold only L3 rules. Blocks 0-2 select templates 0
and 1, block 3 templates 2 and 3, and an L3 rule takes the first
template of its block that has the IPv4 destination (DIP0): template 1,
or template 2 in block 3. Of the fixed templates only template 3 holds
a whole IPv6 destination (DIP0 to DIP7), so an IPv6 rule could only go
to block 3.

Select templates 1 and 3 in blocks 0-3, and let an L3 rule that
matches an IPv6 destination take the template with DIP2, so that IPv4
and IPv6 rules can each use the whole L3 range (#25768). IPv4 rules
now match on template 1 in all four blocks, which holds the IPv4
destination as template 2 did in block 3. Blocks 4 and up keep their
templates.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25838
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-12 18:54:45 +02:00
Gennaro Cimmino 724cf05db2 realtek: pie: rtl839x: read an IPv6 destination back
rtl839x_write_pie_templated() writes an IPv6 destination 16 bits per
template field, DIP0 the lowest and DIP7 the highest. The reader builds
it at DIP2 from whole register words instead, as the RTL838x reader
does, and skips DIP3 to DIP7, so a rule matching an IPv6 destination
reads back with the wrong address and mask. Read each field back into
the 16 bits it was written from; DIP0 and DIP1 fill the IPv6 address as
well as the IPv4 one.

No RTL839x rule matches an IPv6 destination yet; the following patches
add some and dump them through debugfs. On an HPE 1920-48G (RTL8393)
with them the dump showed the switch's IPv6 addresses and prefixes as
configured, e.g. fd00:90:3::1/128 and fd00:90:3::/64.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25838
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-12 18:54:45 +02:00
Quinn O'Shea 303cfa7591 realtek: dsa: refuse VLANs on 802.1ad bridges
The switchdev VLAN object carries no protocol, so a VLAN added on a
bridge with vlan_protocol 802.1ad is programmed as an 802.1Q VLAN
without an error. The switch then matches it against the 802.1Q tag of
a frame, which such a bridge does not treat as its VLAN tag, while the
bridge believes the VLAN is offloaded.

Ask the bridge for its protocol and refuse anything but 802.1Q with an
error and an extack message. -EOPNOTSUPP would not do: the bridge takes
it as "keep this VLAN in software" and carries on silently. A host VLAN
arrives on the CPU port, which is in no bridge, so take the bridge from
the VLAN object there.

A port can then only join an 802.1ad bridge that has no default PVID
and no host VLAN, as the bridge adds those VLANs while the port joins.

Changing the protocol of a bridge that already holds ports is not seen
by the driver, as DSA does not forward that attribute.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Quinn O'Shea <qoshea@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/25835
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-12 18:53:18 +02:00
Gennaro Cimmino 404823678f realtek: l3: rtl839x: route with the egress device's MAC
On RTL839x every routed frame leaves with the switch's own MAC,
whatever device the route goes out of, and frames to a device with a
MAC of its own are not routed: every ROUTING_SA_CTRL entry the driver
writes (0 to 14) holds the switch's MAC, and every ROUTING row names
entry 0 (#25768, items 3 and 4).

The 16 entries are the source MACs a ROUTING row names (SW_MAC_IDX,
Realtek GPL SDK, dal_cypress_l3_routeEntry_set()) and, as measured,
also the MACs whose frames the switch routes. Entry 0 stays the
switch's MAC (dal_cypress_l3_init()). Any other MAC that a route goes
out of a device with takes an entry, per MAC and held by the routes
through it, the on-link prefix included, so a device routes on its MAC
while it has an address, if an entry was free when it got it. A
ROUTING row is now keyed on the gateway's MAC and that entry. Unused
entries hold FF:FF:FF:FF:FF:FE, as the SDK leaves them.

What the hardware cannot do traps. A route through a gateway that
finds no entry free traps, and asks again on the gateway's next
neighbour update. Two devices with different MACs on one VLAN reach a
gateway through one L2 entry, which can name only one of their rows:
the routes already forwarding keep it. A device that takes a new MAC
has its entry rewritten when only its routes hold it and no other
entry has the new MAC; otherwise its routes move to an entry for the
new MAC, those through a gateway trapping until a resolve gives them
their new row, and the old entry, still routed, is given up only with
the rows that name it.

Measured on an HPE 1920-48G (RTL8393), on main f29dfe25ba with the
previous patch and bench debugfs files, before the rebase onto
e5ec5e199f; 8 echo requests per case, forwarding off, switch.3 and
switch.7 with MACs of their own (M3, M7), gateway G in VLAN 3 and, with
the same MAC, in VLAN 7, sending now and then as a live gateway does:
- from VLAN 7 to M7, route out of switch.3: 8/8 at G, TTL 63, source
  M3 (on the #25808 image: 0/8, all to the CPU)
- routes via G out of switch.3 and switch.7: two ROUTING rows, 8/8
  with M3 and 8/8 with M7
- switch.3 down: its entry freed, the switch.7 route still 8/8 with
  M7; up again: M3 back, 8/8
- switch.7 to a new MAC: its entry rewritten in place, 8/8 with the
  new MAC, the switch.3 route unchanged
- switch.7 to M3, which switch.3's entry holds: its routes moved to
  that entry, its old one freed, both routes 8/8 with M3
- with one entry held, 16 more VLAN devices with MACs of their own:
  the entries ran out after 14, and a gateway route out of a device
  without one trapped; once the others were gone it took an entry on
  its next neighbour update (debugfs and register reads, no traffic;
  G was never on those VLANs, so before e5ec5e199f its routes there
  got an invented L2 entry, and on main they trap for want of one:
  only the switch MAC entries were checked)
- rerun from #25805 and #25808: two prefixes in both orders, one
  gateway for two routes, neighbour loss: same results

Other RTL839x boards: routes keep entry 0, as before, while every
device they go out of has the MAC eth0 had at probe. A device with
another MAC now takes an entry, and frames to it are routed by the
switch instead of the CPU.

RTL930x sets no set_switch_mac: its routes take no entry, and its
netdev notifier path is unchanged. Compile-tested on rtl930x. RTL838x
and RTL931x have no setup, so their FIB and neighbour events are ignored
and none of this runs there.

Not tested: two devices with different MACs on one VLAN, a move to a
new entry, a device going back to the switch's MAC, a failed ROUTING
row write.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25822
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-12 13:38:39 +02:00
Gennaro Cimmino b3524cbdfa realtek: l3: give the DMAC entries a callback of their own
The DMAC entries are written through set_egress_mac() and keyed on the
gateway's MAC alone. That is what an RTL930x L3_EGR_INTF_MAC entry
holds, but an RTL839x ROUTING row also names the switch MAC entry a
routed frame leaves with (SW_MAC_IDX), and routing with the MAC of the
device a route goes out of needs a row per gateway MAC and switch MAC
entry.

Give the entries a callback of their own, set_dmac(), which takes what
an entry holds, struct otto_l3_dmac: the gateway's MAC and the switch
MAC entry, which RTL930x does not take. RTL930x writes the entry
through otto_l3_930x_set_egress_mac() as before. RTL839x writes the
ROUTING row with SW_MAC_IDX from it, so set_egress_mac() is RTL930x's
alone now.

No functional change: every entry still holds a switch MAC entry of 0,
so the rows and the DMAC entries written, and which routes share them,
are as they were. Compile-tested on rtl839x and rtl930x; route-tested
on an HPE 1920-48G (RTL8393) together with the following patch, on
f29dfe25ba.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25822
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-12 13:38:39 +02:00
Gennaro Cimmino 13f0c425a0 realtek: l3: move otto_l3_gw_seen() up
A following patch calls otto_l3_gw_seen() from the handling of a
device's MAC change, which sits above it in l3.c. Move it there,
together with otto_l3_route_inside(), which it calls, so that no
forward declaration is needed. No functional change.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25822
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-12 13:38:38 +02:00
Quinn O'Shea 285ff90752 realtek: dsa: rtl930x: leave room for a frame in the egress rate limiter
The burst of the port egress rate limiter is clamped to 64000 bytes.
The bucket of the limiter is 16 bits wide and has to hold the burst and
the frame that is sent. When the two no longer fit, the port is not
limited at all, so that clamp only works for frames of up to 1535
bytes.

On an RTL9303 with a limit of 100 Mbit/s, 400 Mbit/s offered and frames
of 9018 bytes, all 400 Mbit/s pass.

Clamp the burst to what leaves room for the largest frame the switch
forwards.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Quinn O'Shea <qoshea@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/25831
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-12 08:48:37 +02:00
Markus Stockhausen 0a70f32f4b realtek: rt-loader: use -O2 with selective LZMA inlining
Build rt-loader with -O2 and tune the LZMA decoder's inlining decisions
to balance code size and decompression performance.

The existing -O3 setting applies to all loader C sources. In the
standalone decoder comparison used to investigate this change, -O3
substantially increased the generated code size. Simply switching to
-O2 reduced that footprint, but left the frequently used rc_get_bit()
helper out of line, adding function-call and stack-access overhead
throughout range decoding.

Adjust the two helpers independently:

- Declare rc_get_bit() inline to encourage integration into its callers.
  This lets the compiler eliminate call overhead and optimise accesses
  to the range-coder state, probability values and decoded symbols.
- Mark rc_do_normalize() noinline to retain a shared implementation of
  the conditional normalisation path. This limits code duplication as
  the surrounding bit-decoding helpers are inlined.

Use the inline keyword rather than always_inline for rc_get_bit().
With the tested compiler, forcing this helper to inline provided no
additional benefit over the simpler declaration.

A standalone comparison with GCC 13.2.0 targeting MIPS32r2/34Kc used
the same synthetic 1 MiB LZMA stream, producing 4 MiB of output. A
generic single-issue in-order model with a 32 KiB instruction cache
and a 16 KiB data cache produced the following results:

- Original -O2: 4,960 bytes of decoder .text; 548,160,936 model cycles.
- Modified -O2: 4,960 bytes of decoder .text; 389,069,748 model cycles.
- Original -O3: 10,496 bytes of decoder .text; 336,140,240 model cycles.

In that setup, the modified -O2 decoder retains approximately 75% of
the cycle reduction achieved by -O3 while using 52.7% less code.
It requires 15.7% more model cycles than -O3. Compared with unmodified
-O2, it reduces model cycles by 29.0% without increasing code size.
All compared variants produced byte-identical output.

These figures describe the standalone decoder experiment, not measured
Realtek hardware performance. The experiment used -fno-pic and
-mno-abicalls; rt-loader retains -fpic and -mabicalls. Actual loader
size and boot-time effects therefore require validation with the
target build and hardware.

The patch changes the optimisation level for the whole loader and
the inlining policy of these two decoder helpers. It leaves the
decompression algorithm, bitstream format and error-handling logic
unchanged.

Assisted-by: ChatGPT (OpenAI GPT-6 Astra)
Link: https://github.com/openwrt/openwrt/pull/25813
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-12 08:41:31 +02:00
Gennaro Cimmino e5ec5e199f realtek: l3: rtl839x: do not invent a next-hop L2 entry
On RTL839x a route through a gateway the switch holds no L2 entry for
gets one invented by otto_l2_nexthop_add(): static, with the don't-care
port. The gateway is then cut off from the switch, and what the route
should forward goes to the CPU instead. Measured on an HPE 1920-48G
(RTL8393), main f29dfe25ba, gateway G on a bridge VLAN behind lan48,
with a permanent neighbour and a MAC the switch had not seen, 8 echo
requests to a destination behind it, forwarding off:
- G's L2 entry was written static, port 63
- G got 0 of 3 replies from the switch; the 8 echo requests reached
  the CPU, none reached G
- with the neighbour made dynamic the entry stayed as it was
- with the route removed G got 3 of 3 replies, and the switch learnt
  it on port 47
Setting lan48's static move action to forward did not restore it, so
that alone does not explain it; the mechanism is not known.

RTL930x stopped inventing that entry in 138fabb79f ("realtek: l3: do
not invent an L2 entry for a next hop") wherever a route can trap, and
since #25805 an RTL839x route can. Require an entry the switch already
holds on every family: the route traps until the switch learns the
gateway from the traffic the CPU routes, and a later neighbour update
programs it. Nothing asks otto_l2_nexthop_add() to invent an entry any
more, so the flag and the invented entry go. A route trapped for want
of one is now logged on RTL839x as on RTL930x. On RTL839x an L2 entry
that is a next hop already is now pointed at its gateway's ROUTING row
as on RTL930x, one row per gateway MAC since c282dd1f4d.

The same case with this change: no entry written, the route trapped,
G got 3 of 3 replies and was learnt on port 47; the 8 echo requests
reached the CPU and, with forwarding on, G through the kernel, TTL 63.
With the neighbour made dynamic the route forwarded on the learnt
entry, 8 of 8 at G, none at the CPU. The cases from #25805 rerun (two
prefixes in both orders, neighbour loss): same results. Since that run
only the log line and comments changed.

As 138fabb79f says, a route through a permanent neighbour the switch
had not learnt stays with the CPU, since such a neighbour sees no
update: measured here too, it stayed trapped after G was learnt, until
the neighbour was made dynamic. Also as on RTL930x, a route that claims
an entry the switch learnt again after a flush counts itself on top of
the count the flushed entry left, so that entry can stay a next hop
once the route is gone; not tested.

RTL930x: no change, it already required such an entry. RTL838x and
RTL931x have no setup, so their FIB and neighbour events are ignored and
none of this runs there. Compile-tested on rtl930x.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25824
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 22:45:51 +02:00
Markus Stockhausen 62efc09fda realtek: dsa: rename VLAN info structure to otto_vlan_info
Rename struct rtldsa_vlan_info to struct otto_vlan_info and update
all references to match the otto_vlan_ namespace.

No functional changes intended.

Assisted-by: ChatGPT (OpenAI GPT-6 Astra)
Link: https://github.com/openwrt/openwrt/pull/25817
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 20:48:14 +02:00
Markus Stockhausen 6ba354603e realtek: dsa: use otto_stats prefix for statistics data
The statistics functions already use the otto_stats_ prefix, while
the associated structure types and variables still use rtldsa_.

Rename the counter and MIB structure types, per-SoC MIB lists and
descriptors, and polling interval macros to match the module's
namespace. Update all declarations and references in the shared
header and the four SoC configurations.

No functional changes intended.

Assisted-by: ChatGPT (OpenAI GPT-6 Astra)
Link: https://github.com/openwrt/openwrt/pull/25817
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 20:48:14 +02:00
Gennaro Cimmino aef4cf2215 realtek: l3: rtl839x: trap packets to an aged-out gateway
On RTL839x a route forwards through its gateway's L2 entry. A gateway
that sends nothing lets that entry age out, and a packet routed
through it then hits the routing entry age-out exception, whose action
is forward after reset: the packet is routed and also reaches the CPU,
which routes it a second time when IPv4 forwarding is on.

Trap it instead (ROUTING_AGE, bits 15:14 of ROUTING_EXCPT_CTRL: 0 drop,
1 forward, 2 trap; Realtek GPL SDK, dal_cypress_trap.c, exception
ROUTE_EXCEPTION_TYPE_ENTRY_AGE_OUT), as the TTL exception already does.
The CPU routes such packets alone until the gateway sends again and
its entry is learnt anew.

Measured on an HPE 1920-48G (RTL8393), 10.99.0.0/16 via a gateway
that sends nothing, 8 echo requests per case, once the age of its L2
entry had run down to 0 (about 4.5 minutes on the bench):
- without this change: forwarding off, 8/8 routed and 8/8 at the CPU
  as well; forwarding on, 14 at the gateway, 8 from the switch and 6
  from the kernel
- with it: forwarding off, none routed, 8/8 at the CPU; forwarding
  on, 8 at the gateway, none of them twice
With a fresh entry, 8/8 were routed in hardware and none reached the
CPU, without and with this change. Both images also carried the
pending switch MAC change for #25768 items 3 and 4 and bench debugfs
files.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25821
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 20:46:51 +02:00
Gennaro Cimmino 52852ca38b realtek: l3: rtl839x: match only IPv4 frames in route rules
The RTL839x route rules match the destination in the template's DIP
fields, and the trap rules the switch MAC too, but never the frame
type. An IPv6 frame fills those fields too, so an
IPv6 frame to the switch whose destination ends in the 32 bits of an
offloaded IPv4 prefix takes that prefix's rule and is routed in
hardware, as IPv6, to the IPv4 route's gateway. IPv4-embedded IPv6
addresses under a /96 prefix (RFC 6052), such as the NAT64 prefix
64:ff9b::/96, end that way.

Match FRAME_TYPE IPv4 (2, USER_FIELD_FRAME_TYPE in the Realtek GPL
SDK), which the IACL entry keeps at bits 337:336 with its mask at
121:120, where the PIE writer already puts frame_type.

Measured on an HPE 1920-48G (RTL8393), 10.99.0.0/16 offloaded via
gateway G, forwarding off, 8 IPv6 echo requests to the switch for
2001:db8:5::a63:601 to ...:608, whose low 32 bits are 10.99.6.1 to .8:
- without this change: 8/8 routed to G (hop limit 63), the rule's
  packet counter +8
- with this change: none at G, 8/8 at the CPU, counter +0
Destinations with 10.99.6.x in their high 32 bits were not routed on
either. IPv4 routing through the rules was unchanged (prefix order,
neighbour loss and back, a gateway shared by two routes). Both images
were main c282dd1f4d with bench debugfs files and the switch MAC change
for items 3 and 4 of #25768, not posted yet (a later revision of it on
the second, differing in how routes follow a MAC change).

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25818
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 19:01:46 +02:00
Gennaro Cimmino c282dd1f4d realtek: l3: rtl839x: keep one ROUTING row per gateway MAC
On RTL839x a gateway's L2 entry names the ROUTING row that holds the
gateway's MAC, and that row was the id of the route that claimed the
entry. The other routes through that gateway go on using the row after
the route is gone, and its id can go to a later route, which writes
its own gateway's MAC into it. Those routes then forward to the wrong
gateway.

Give RTL839x the DMAC entries RTL930x has: one ROUTING row per gateway
MAC, held by every route through it (otto_l3_dmac_get()) and written
when it is first taken. The bookkeeping takes 24 KiB, allocated at
probe as on RTL930x.

Measured on an HPE 1920-48G (RTL8393) with #25805 on main 17a3a87f6e,
8 echo requests per case, forwarding off: routes A and B via gateway
G, A removed, then route C via gateway H.
- without this change, C took A's row and B's traffic arrived 8/8 at
  H, sent to H's MAC
- with it, C got a row of its own and B's traffic stayed 8/8 at G
Of the #25805 bench, prefix order in both orders, blackhole, neighbour
loss and back, and 22 routes in random order gave the same results as
there.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25808
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 16:45:42 +02:00
Gennaro Cimmino 9f6780e968 realtek: l3: rtl839x: order route rules and add traps
On RTL839x every offloaded route is a PIE rule that matches the
destination prefix, and of the rules that match, the lowest index
acts. rtl839x_pie_rule_add() hands out the first free index, so a /16
added before a /24 inside it takes the /24's traffic. Nothing traps
either, so the switch forwards what the kernel would not: a blackhole
inside an offloaded prefix, a local prefix, routes under policy rules,
a gateway whose neighbour is gone (#25768, items 5 to 9).

Handle the rules the way the RTL930x prefix table is handled. The
rules pie.c keeps for L3 are the prefix rows: a route takes one when
it is added, longest prefix first, the local table ahead of main. The
ROUTING row with the gateway MAC gets a writer of its own. A move
writes the routes again at their new rows, since a rule holds nothing
its route does not. A route the switch must not forward redirects to
the CPU port, and only frames addressed to one of the switch's MACs
(SWITCHMAC), so a frame for another station is not taken.

The traps for routes left out, local prefixes, policy rules and a lost
neighbour now run on RTL839x too. Policy rules are checked for IPv4
only, as IPv6 is not offloaded there; the check replaces the "FIB4
default rule failed" message. A gateway whose MAC was not learnt on a
port still forwards, as before, and a lost event is still not
replayed. The debugfs route dump walks the L3 rules and shows the
trapping ones.

Measured on an HPE 1920-48G (RTL8393), RAM-booted from main
17a3a87f6e plus this series, 8 echo requests per case, the client and
both gateways behind one port in VLANs 7 and 3:
- /16 via A and /24 via B, added in both orders, forwarding off: /24
  traffic 8/8 at B, the rest 8/8 at A, TTL 63, none at the CPU
- blackhole /24 inside the /16: none forwarded, 8 at the CPU
- a rule sending a /24 to table 100: offload suspended, the kernel
  sends 8/8 to that table's gateway; back in hardware once it goes
- local 10.99.0.0/16 in table local: 8/8 answered by the switch
- gateway neighbour failed: 3/3 Destination Host Unreachable, back in
  hardware once the gateway answers again
- 22 routes added in random order, half removed, then the rest: rows
  dense and longest prefix first after each phase
The network (re)starts suspended the offload for about 10 ms each, as
the policy rules changed.

RTL930x: no change. Every changed check tests a flag or callback that
RTL930x sets, and the new row bound in otto_l3_route_place() lies
past the IPv6 catch-all row, which stops the IPv4 rows first. RTL838x
and RTL931x have no setup, so their FIB events are still ignored.
Compile-tested on rtl930x.

Not tested: a gateway MAC change, routes past rule 127 (the order
across blocks was checked by hand, #25768).

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25805
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 13:50:33 +02:00
Gennaro Cimmino b98061ac6e realtek: pie: rtl839x: keep the first 512 rules for L3
L3 is to keep its RTL839x routes longest prefix first, the way the
RTL930x prefix table holds them, so it needs rules that nothing else
hands out and that it writes in place. Mark blocks 0-3 as used, so
rtl839x_pie_rule_add() never returns one of them, and add a writer
for them that takes the template of the block holding the IPv4
destination.

Until L3 uses the writer, its rules come from rtl839x_pie_rule_add()
from block 4 on, where template 2 holds the destination too. Nothing
else adds RTL839x rules: tc offload is wired for RTL93xx only.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25805
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 13:50:33 +02:00
Gennaro Cimmino 011e33d47f realtek: l3: trap a route whose L2 next hop was not written
otto_l2_nexthop_add() fails when the gateway finds no free L2 slot, or,
where the next hop has to exist already (RTL930x), when the gateway's
address has not been learnt. The route then still forwards, through
the L2 index it held before, or index 0 on its first update.

Let it trap instead. RTL930x behaves as before: the call fails before
it writes nh->port, which stays port_ignore, so no_port already traps
the route. On RTL839x the PIE rule forwards whatever the route says
until the rules can trap, so nothing changes there either.

Not tested on hardware: no failed next hop write was provoked.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25805
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 13:50:33 +02:00
Markus Stockhausen c8fbe693c3 realtek: dsa: consolidate statistics handling in stats.c
Move hardware statistics helpers and packet counter allocation, read
and clear functions into stats.c.

Standardize function names on the otto_stats_ prefix, followed by the
chip identifier where applicable, and remove redundant stats suffixes.
Update declarations, includes, callers and operation tables accordingly.

No functional changes intended.

Assisted-by: ChatGPT (OpenAI GPT-6 Astra)
Link: https://github.com/openwrt/openwrt/pull/25802
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 13:41:18 +02:00
Markus Stockhausen 74521f894e realtek: dsa: consolidate L2 helpers in l2.c
Move the hash index, hash/CAM lookup, unicast reference lookup and
ageing-time helpers into l2.c. This removes the dependency of L2 code
on lookup helpers implemented in dsa.c.

Rename the moved functions with the otto_l2_ prefix, place their
public declarations in l2.h and update includes and callers.

Leave DSA callbacks and port/bridge handling in place.
No functional changes intended.

Assisted-by: ChatGPT (OpenAI GPT-6 Astra)
Link: https://github.com/openwrt/openwrt/pull/25802
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 13:41:18 +02:00
Markus Stockhausen 174f1180a4 realtek: dsa: move remaining LAG handling to lag.c
LAG handling is split between dsa.c and lag.c. Consolidate the remaining
LAG callbacks and helpers in lag.c to keep related functionality together.

Rename the moved functions with the otto_lag_ prefix and declare the
cross-file interfaces in lag.h. Update callers, DSA operation tables and
RTL93xx configuration references, and add the required includes.

Leave the port management functions in place for a later refactoring.
No functional changes intended.

Assisted-by: ChatGPT (OpenAI GPT-6 Astra)
Link: https://github.com/openwrt/openwrt/pull/25802
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 13:41:18 +02:00
Quinn O'Shea 7f31e15de4 realtek: dsa: map DSA LAG ids to zero-based trunk groups
DSA numbers LAGs from 1 to num_lag_ids. The driver uses the id as it is
to index its LAG arrays and as the hardware trunk group, both of which
start at 0. Group 0 is therefore never used and the last id is refused,
so one LAG fewer than the switch supports can be offloaded.

Accept ids from 1 to num_lag_ids and subtract one where the id becomes
a group.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Quinn O'Shea <qoshea@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/25792
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 08:54:06 +02:00
Quinn O'Shea 2433508b51 realtek: dsa: report a failed LAG leave
port_lag_leave returns 0 whatever happened, and it clears the port from
the driver's LAG masks before it asks the hardware to release it. When
the release fails, DSA is told that it worked and the driver no longer
counts the port as a LAG member while the hardware still does.

Release the port in hardware first, update the masks only when that
succeeded, and return the error, which DSA then logs.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Quinn O'Shea <qoshea@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/25792
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 08:54:06 +02:00
Quinn O'Shea 8509328d1a realtek: dsa: track the primary port of a LAG
The first member of a LAG becomes its primary port when lag_primary[]
holds -1, but the array comes zero-filled from devm_kzalloc() and is
never set to -1. Every member is therefore marked non-primary, and the
FDB and MDB code of the RTL83xx skips non-primary ports.

The leave path is wrong as well: it elects a new primary whenever any
member leaves, uses fls64(), which is one-based, as a port number, and
keeps the old primary when the last member leaves, so the next LAG with
that id starts without one.

Set every slot to -1 at probe, record the primary only after the
hardware join succeeded, and on leave elect a new primary only when the
primary itself leaves: the lowest remaining member, or -1 when the LAG
is empty.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Quinn O'Shea <qoshea@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/25792
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 08:54:06 +02:00
Quinn O'Shea c39546671e realtek: dsa: take the LAG id from the notifier
port_lag_leave looks the LAG up with dsa_lag_id(). DSA drops its
reference on the LAG before it notifies the switch, so when the last
port leaves, the LAG is already unmapped and the lookup returns -ENODEV.

Without this change, the leave is rejected as "LAG -19 invalid" and the
port stays a member of the hardware trunk. Addresses learned on it
afterwards are stored as trunk entries and frames for them are no
longer delivered, and the port cannot join a LAG again.

DSA passes the id in the notifier for exactly this reason. Use it in
port_lag_leave, and in port_lag_join for symmetry, and drop the check
for a return value dsa_lag_id() never had.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Quinn O'Shea <qoshea@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/25792
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 08:54:06 +02:00
Markus Stockhausen 60391538c7 realtek: dsa: standardize TC function prefixes
TC functions currently use a mix of rtl83xx_ and rtldsa_ prefixes.
Use otto_tc_ for generic functions and otto_tc_930x_ or otto_tc_931x_
for device-specific functions to make TC module ownership explicit.

Remove redundant tc components from the remaining function names.
Update all declarations, call sites, callback assignments and
function references in comments accordingly.

Assisted-by: ChatGPT (OpenAI GPT-6 Astra)
Link: https://github.com/openwrt/openwrt/pull/25790
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 08:48:03 +02:00
Harm Kroon 385021509d realtek: fix race condition in rtl8214fc_media_set
There is a similar race condition in rtl8214fc_media_set() as in
rtl8214fc_power_set(). It changes the base PHY's page registers in
separate MDIO bus lock sections, so another port can see the internal
page in between and restore it.

This fix holds the MDIO bus lock for the whole page-select sequence in
rtl8214fc_media_set().

Signed-off-by: Harm Kroon <hhhkroon@hotmail.com>
Link: https://github.com/openwrt/openwrt/pull/25766
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 08:47:05 +02:00
Harm Kroon 9ae0837c58 realtek: fix race condition in rtl8214fc_power_set
There is a race condition in rtl8214fc_power_set(). It reads the
extended page register (reg 30) with __phy_read() without the MDIO bus
lock. Port 0 of the RTL8214FC is the base PHY. The other ports
poll __rtl8214fc_media_is_fibre(), which sets the base PHY's reg 30 to 8
(INTERNAL) for a short time. If port 0 reads in that window, it
"restores" page 8 and stays there. Its standard registers then point to
the internal page, so no link is seen. Every later power cycle reads 8
and writes 8 again.

This fix holds the MDIO bus lock for the whole page-select sequence in
rtl8214fc_power_set().

Signed-off-by: Harm Kroon <hhhkroon@hotmail.com>
Link: https://github.com/openwrt/openwrt/pull/25766
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-11 08:47:05 +02:00
Gennaro Cimmino baf6e5319b realtek: pie: rtl839x: place the log index at bit 6
A PIE rule on RTL839x counts the packets it matches into the counter
named by its LOG_IDX field, bits 80:70 of the entry (Realtek GPL SDK,
rtk_cypress_tableField_list.c), which are bits 16:6 of r[14]. The
writer puts the index at bits 14:4, so the hardware reads it divided
by four, and bit 0 of the index lands in MIR_IDX.
The reader shifts the same way, so the route dump in debugfs reads the
counter the driver allocated rather than the one the hardware fills.

Write and read the index at bit 6.

Tested on an HPE 1920-48G (RTL8393) with two offloaded routes, which
got packet counters 0 and 1, and 8 echo requests through one route at
a time. Before, the requests through the second route added 8 to the
first route's counter and nothing to its own. After, each route's
counter grew by 8 for its own requests and stayed put for the other's.
Counters above 1 were not exercised.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25795
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-10 21:36:10 +02:00
Gennaro Cimmino 1583716e42 realtek: l3: rtl839x: trap packets whose TTL runs out
What an RTL839x does with a routed IPv4 packet whose TTL runs out is
set by the IP4_TTL_EXECEED field of ROUTING_EXCPT_CTRL, and nothing in
the driver writes that register. On main, #25768 found that an echo
request with TTL 1 through an offloaded route got no Time Exceeded
from the switch, and nothing reached the gateway either.

Set the field to trap, so that the CPU gets the packet and the kernel
answers it as it would on a route it forwards itself. The encoding,
0 drop, 1 forward, 2 trap, is the one of the Realtek GPL SDK
(dal_cypress_trap.c). The other fields of the register keep their
value.

Tested on an HPE 1920-48G (RTL8393), with 10.99.1.0/24 offloaded
through a gateway behind the client's own port and 8 echo requests
with TTL 1 per case, in one boot. With the field at 2, the client got
7 Time Exceeded replies, consistent with the kernel's ICMP rate limit,
and none of the requests reached the gateway. With the field forced
to 0 through devmem, the client got no reply and nothing reached the
gateway, the symptom #25768 reported on main. Echo requests with the
default TTL still reached the gateway 8/8, TTL 63, with IPv4
forwarding off.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25794
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-10 21:35:17 +02:00
Gennaro Cimmino eb51d41b12 realtek: dsa: let the FDB dump walk reschedule
rtldsa_port_fdb_dump() calls cond_resched() every 64 rows, but at the
end of the loop, after the "continue" that skips each row that is not
a valid unicast entry. So it only reschedules when such a 64th row
happens to hold a valid unicast entry.

DSA calls the function once per port. On the 52-port HPE 1920-48G
(RTL8393) a full "bridge fdb show" walks the 16384 rows 52 times:
96 ms per port, 5.0 s in all. With PREEMPT_NONE no other task runs
on that CPU meanwhile. When the kernel's watchdog worker, which
toggles the board's external PT7A7514, waits on that CPU, it misses
its pings and the board resets during the dump, without any console
output.

Move cond_resched() to the top of the loop, as the L3 debugfs dumps
already do, so it runs every 64 rows whatever the row holds.

Tested on the HPE 1920-48G under ping load, with full dumps. Without
this change and the dump pinned to CPU0, 20 of 20 dumps completed
with the watchdog worker on CPU1; moved to CPU0 in the same boot, the
board reset on the first dump. With this change, 20 of 20 completed
with both on CPU0, 20 of 20 with CPU1 kept busy and 20 of 20 with
nothing pinned. Across those 60 dumps no two toggles of the watchdog
GPIO were more than 0.51 s apart. Not tested on other families.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25787
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-10 18:57:18 +02:00
Quinn O'Shea 7c8ca38c76 realtek: dsa: report multicast and broadcast frame counts
The port counters hold the multicast and broadcast frame counts, and
the frame totals are already built from them, but the fields that
report them on their own are never assigned. ethtool leaves the
multicast and broadcast members of the standard MAC statistics out, and
the multicast member of the link statistics reads zero.

Assign them from the counters that are already collected.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Quinn O'Shea <qoshea@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/25756
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-10 17:12:45 +02:00
Gennaro Cimmino 5d9fa15647 realtek: eth: drop the RTL839x L2 notification code
On RTL839x the ethernet driver turns on the L2 notification engine and
passes its learn and age events to the bridge. It reports them against
the conduit, which is not a bridge port, so br_switchdev_event() drops
every one of them. Nothing else reads them. RTL838x, RTL930x and
RTL931x run without this code.

Drop the notification setup, its interrupt handling and ring, and the
NIC reset code that preserved them. The vendor SDK defines no
interrupt in the mask bits this code also set on the other families:
bits 20-22 of DMA_IF_INTR_MSK on RTL838x, bits 4-6 of
DMA_IF_INTR_TX_DONE_MSK on RTL930x and RTL931x.

Tested on an HPE 1920-48G (RTL8393):
- after boot, L2_NOTIFICATION_CTRL reads 0x00001002 (engine off) and
  DMA_IF_INTR_MSK has none of bits 20-22 set;
- the switch still learns the hosts behind its ports;
- a route offloaded with IPv4 forwarding off still delivers 8 of 8
  echoes.

This does not stop the resets on a full bridge fdb show on that switch:
they happen with or without this code.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25776
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-10 16:40:42 +02:00
Gennaro Cimmino 884ab21a3a realtek: l3: add a route dump to debugfs for RTL839x
An RTL839x has no L3 tables to dump. It routes through PIE rules that
match the destination prefix and forward to the L2 entry of the
gateway, and that entry names the ROUTING table row holding the gateway
MAC. Add realtek_otto_l3/routes for it: it walks the PIE rules and
shows each routing one with its L2 entry, its ROUTING row and its
packet counter.

The setup of the debugfs directory moves into a helper that both
families share.

Tested on an HPE 1920-48G (RTL8393) with 10.99.0.0/16 via one host
and 10.99.5.0/24 via another:
- both rows show the gateway's MAC in the L2 entry and in the ROUTING
  row, on the port that host is behind;
- 8 echoes into each prefix raise the counter of the rule that matched
  by 8;
- the counters show the /16 rule, installed first, matching 10.99.5.x
  as well, and captures saw all 8 of those echoes at the /16 gateway;
- after deleting the /24 route, only the /16 row is left.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25775
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-10 16:33:55 +02:00
Gennaro Cimmino 86cc0a4188 realtek: pie: rtl839x: read rules back as they are written
rtl839x_pie_rule_read() does not read back what rtl839x_pie_rule_write()
writes:
- it takes the forward action, and the drop, from r[14], while the
  writer puts them in the low bits of r[13];
- it never reads the forward and log data;
- it reads the masks of the templated fields word aligned, while the
  writer puts them 8 bits up;
- it puts the two halves of the destination address in the wrong
  order and builds the mask from the address.

Read all of these back from where the writer puts them. The hardware
follows the writer: the routes it programs on an HPE 1920-48G (RTL8393)
forward to their next hop.

Nothing reads a PIE rule back on RTL839x yet, so this changes no
behaviour. The RTL839x L3 debugfs added next uses it.

Checked on that switch through the debugfs dump of the next commit:
- the rules of 10.99.0.0/16 and 10.99.5.0/24 read back as these
  prefixes, with the L2 index of their gateway;
- their raw words hold forward action 6 in the low bits of r[13];
- their masks sit 8 bits up, as written.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25775
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-10 16:33:55 +02:00
Gennaro Cimmino c09cb2f80f realtek: dsa: rtl839x: add a port to its own isolation mask
The RTL839x applies a port's isolation mask to every frame that came in
on it, routed ones included. The driver leaves the port out of its own
mask, so a route offloaded through a gateway behind the port a packet
came in on, such as routing between two VLANs of one trunk, drops
everything: the kernel shows the route as offloaded, and the drop
counter PORT_ISOLATION counts each packet.

Keep the port in its own mask, as the GPL SDK does at init
(_dal_cypress_port_init_config() sets every port's mask to all ports).
A broadcast is still not sent back out of the port it came in on. Only
RTL839x changes; the other families keep their masks as they are.

Tested on an HPE 1920-48G (RTL8393), with 10.99.1.0/24 routed from VLAN
7 to a gateway on VLAN 3, both behind lan48. On main, 0/8 echo requests
reached the gateway, even with IPv4 forwarding on in the kernel. With
this change and forwarding off, so that only the hardware path delivers,
8/8 arrived, TTL 63, from the switch MAC. Routing to a gateway behind
lan47 still delivers 8/8. Two bursts of 50 broadcasts entering lan48
added nothing to its tx_BroadcastPkts, while the same entering lan47
added 50 and 53.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Gennaro Cimmino <gcimmino@rayonra.net>
Link: https://github.com/openwrt/openwrt/pull/25764
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-09 22:40:46 +02:00