Gennaro Cimmino 99b56c4deb realtek: l3: rtl930x: offload IPv6 unicast routes
The IPv6 half of the L3 tables is written and read by the same code that
drives the IPv4 half, and the setup enables it in hardware, but nothing
has ever produced a route for it: the FIB notifier dropped every IPv6
entry event and the handler behind it was a stub.

Take those events, hold the route across the work item that handles
them, and program the prefix table from them the way the IPv4 handler
does, minus the host route table: the prefix entry carries a host-route
bit of its own, which the writer already sets for a prefix length of
128, so one table serves every prefix length.

Offloading is refused for a nexthop object, a source-specific route, a
multipath route, anything that is not plain unicast, a route with no
gateway, and the two destinations no forwarding row should match: the
default route, which would take everything, and the loopback address.

Refused is not the same as left alone. A shorter prefix that is
offloaded still matches such a destination and forwards it, whatever the
route says: a blackhole /64 under an offloaded /48 would be forwarded to
the /48's gateway. So a refused route gets a row of its own that traps
to the CPU, which routes it as the kernel does. The default route gets
none, since that row would match every destination; the catch-all row
added later in this series traps what no other row matches. A multicast
route gets nothing either. A delete takes that row out before anything
else, since the route it belonged to was never judged offloadable. The
cost is an entry per such route; on the bench, the connected prefixes,
fe80::/64 and the ULA prefix netifd adds as unreachable, five entries in
all. A multipath route keeps its row until its last member is gone: an
add carries the route with every sibling it has, a delete names only the
members that leave, so the row counts them. Source-specific routes to
one prefix share its row, which counts the members of the last one to
arrive, so deleting one can take the row from the others; the driver
logs an error for every source-specific route it traps.

An add arrives as a replace even when it is the only word the kernel
sends about a programmed route going away, so the row for a destination
is taken out before the route replacing it is judged, and a delete is
matched on its gateway as well, because entries that share a destination
arrive with their sibling count already cleared. A multipath route that
shrinks back to a single one stays refused until something touches it
again: the only word about it names the member that left.

A route takes its row as soon as it is added, as a trap: until its
gateway answers, a shorter prefix in hardware would otherwise forward
what this route has yet to resolve. It holds the FIB entry it came from
until the row goes, and is reported as trapping until the gateway
answers and the row forwards, then as offloaded; one that finds no free
row is reported as failed. One whose gateway answered on no port the
switch knows keeps trapping, so it is reported as trapping, not
offloaded, until an update gives it a port. So is one whose gateway's
neighbour stops being valid, until it is valid again.

That leaves the switch's own addresses. A packet whose destination MAC
is the switch's own terminates on the router MAC table and is then
looked up as a route, and that table carries no IP address, so the
hardware has no notion of which addresses are its own: such a packet
misses the lookup and is dropped. The IPv4 path mostly escapes this,
because it writes a host route trapping each of its own addresses that
sits on a device with a switch port below it, and because ARP is not IP
and the setup traps non-IP traffic separately. Neighbour discovery is
ICMPv6, so IPv6 has neither.

What a neighbour sends to a multicast address is untouched, never
carrying the switch's MAC, so a gateway is still learned from an
unsolicited advertisement. Every unicast step after that is not: the
solicitation addressed to the switch, and the reachability probe that
follows, both carry its MAC and both die in the lookup, so the
neighbour ends up FAILED all the same.

So give every local IPv6 address a prefix route of its own that traps to
the CPU, which is how the SDK asks for one - an entry with no next hop
and the trap action. On a device with forwarding on, the kernel also
joins the subnet-router anycast address of every prefix shorter than a
/127 whose network part is not all zeroes, link-local ones included,
and names its route RTN_ANYCAST rather than RTN_LOCAL; that address is
the switch's as much as the others, so it is trapped with them.

Measured on an RTL9303 with the offload built in, which no subtarget
config does: the switch answers on its own address and on that anycast,
five of five each, and the anycast row comes back with its hit bit set.
With fd00:99::/48 offloaded and fd00:99:0:1::/64 a blackhole, the /64
gets a trap row and pings into it no longer reach the /48's gateway, 0
of 5, while the rest of the /48 still does, 5 of 5; with the blackhole
deleted, its row goes and the same pings arrive, 5 of 5. A /64 through a
gateway that never answers, under such a /48, has its trap row from the
moment it is added: none of five pings reaches the /48's gateway and the
sender is told the address is unreachable, where without that row the
/48 forwarded all five. A multipath /48 under an offloaded /32 keeps its
trap row when one member is deleted, so its traffic follows the member
left, five pings of five, instead of the /32's gateway; and an address
that becomes the switch's own where a route through a gateway held it
gets its trap row, and the switch answers on it.

An address the box holds on several devices at once reaches the driver
once, for whichever of them the kernel made the leaf of that
destination: a bridge, its VLAN interfaces and the conduit all carry the
same link-local, and the one that wins need not be a switch port. So
which device holds an address says nothing about whether it has to be
trapped, and none of them is asked. While a copy remains the kernel
replaces the entry rather than deleting it, so the row is written once
and removed when the last copy goes. An address arrives before the
replace that takes out a route already holding its destination, so only
a row that traps is kept; any other row there is taken out first. Each
row costs one of the 127 slots the table has for an IPv6 entry, taken
from the end the IPv4 rows grow towards; an address that finds none is
left without a row, which makes it unreachable from the switch ports,
and dmesg is the only sign of it.

Telling a row from another takes the next hop index it carries: the id
of the route that wrote it once its gateway answered, and zero on a row
that only traps. So the test that says whether a row is still the route
being removed now compares the action as well, and the index with the
one the route itself wrote rather than with its id - on the removal path
both address families share.

The per-event warning the notifier used to print for an IPv6 entry goes
with the branch that printed it.

Everything IPv6-specific sits behind IS_REACHABLE(CONFIG_IPV6): the FIB
event branch, the FIB entry a route holds and its report. A kernel with
CONFIG_IPV6=n builds and links, and never sees an IPv6 route.

Driver removal gains a wait on the work queue and a pass over the route
list that releases the FIB entries still held. otto_l3_remove() is on
the unbind path of every family, so both run there: the pass finds
nothing to release where no route ever held an entry, and the wait is
for work that family queues on the same single-threaded queue.

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/25313
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
2026-10-03 15:34:36 +02:00
…
…

OpenWrt logo

OpenWrt Project is a Linux operating system targeting embedded devices. Instead of trying to create a single, static firmware, OpenWrt provides a fully writable filesystem with package management. This frees you from the application selection and configuration provided by the vendor and allows you to customize the device through the use of packages to suit any application. For developers, OpenWrt is the framework to build an application without having to build a complete firmware around it; for users this means the ability for full customization, to use the device in ways never envisioned.

Sunshine!

Download

Built firmware images are available for many architectures and come with a package selection to be used as WiFi home router. To quickly find a factory image usable to migrate from a vendor stock firmware to OpenWrt, try the Firmware Selector.

If your device is supported, please follow the Info link to see install instructions or consult the support resources listed below.

An advanced user may require additional or specific package. (Toolchain, SDK, ...) For everything else than simple firmware download, try the wiki download page:

Development

To build your own firmware you need a GNU/Linux, BSD or macOS system (case sensitive filesystem required). Cygwin is unsupported because of the lack of a case sensitive file system.

Requirements

You need the following tools to compile OpenWrt, the package names vary between distributions. A complete list with distribution specific packages is found in the Build System Setup documentation.

binutils bzip2 diff find flex gawk gcc-6+ getopt grep install libc-dev libz-dev
make4.1+ perl python3.8+ rsync subversion unzip which

Quickstart

  1. Run ./scripts/feeds update -a to obtain all the latest package definitions defined in feeds.conf / feeds.conf.default

  2. Run ./scripts/feeds install -a to install symlinks for all obtained packages into package/feeds/

  3. Run make menuconfig to select your preferred configuration for the toolchain, target system & firmware packages.

  4. Run make to build your firmware. This will download all sources, build the cross-compile toolchain and then cross-compile the GNU/Linux kernel & all chosen applications for your target system.

The main repository uses multiple sub-repositories to manage packages of different categories. All packages are installed via the OpenWrt package manager called opkg. If you're looking to develop the web interface or port packages to OpenWrt, please find the fitting repository below.

  • LuCI Web Interface: Modern and modular interface to control the device via a web browser.

  • OpenWrt Packages: Community repository of ported packages.

  • OpenWrt Routing: Packages specifically focused on (mesh) routing.

  • OpenWrt Video: Packages specifically focused on display servers and clients (Xorg and Wayland).

Support Information

For a list of supported devices see the OpenWrt Hardware Database

Documentation

Support Community

  • Forum: For usage, projects, discussions and hardware advise.
  • Support Chat: Channel #openwrt on oftc.net.

Developer Community

License

OpenWrt is licensed under GPL-2.0

S
Description
This repository is a mirror of https://git.openwrt.org/openwrt/openwrt.git It is for reference only and is not active for check-ins. We will continue to accept Pull Requests here. They will be merged via staging trees then into openwrt.git.
Readme BSD-2-Clause BSD-3-Clause GPL-1 GPL-2 ISC Linux-syscall-note MIT
1.2 GiB
0 Stars 1 Watchers 0 Forks
Languages
C 60.6%
Makefile 23.1%
Shell 8.1%
Perl 2.6%
UnrealScript 2.4%
Other 3%