Skip to content

feat(switch): L2 client inventory from the bridge FDB - #538

Merged
gnacho merged 3 commits into
mainfrom
feat/520-fdb-l2-clients
Oct 16, 2026
Merged

gnacho merged 3 commits into
mainfrom
feat/520-fdb-l2-clients

Conversation

@gnacho

@gnacho gnacho commented Oct 16, 2026

Copy link
Copy Markdown
Owner

New "Connected devices" view for the switch role: the layer-2 client inventory from the bridge FDB. There is no DHCP/IP semantics here - a switch only knows the MAC, the port it was learned on and the VLANs it carries, which is exactly what a switch admin needs (and what #518 hid when it removed the IP-oriented Clients page from switches).

  • Read over netlink AF_BRIDGE, dumped per bridge port: a dump filtered by the bridge's own index returns only the bridge's local entries (verified on a live router: 0 learned entries that way, 60 per port). The port of every entry is known by construction.
  • Grouped by (MAC, port) with accumulated VLANs: the same MAC on two ports stays as two rows (loop detection), one MAC on several VLANs collapses into one row. The VLAN 0 "unfiltered" marker is dropped when real VLANs exist.
  • A port's own MAC is excluded (hostapd publishes the BSSID as a static FDB entry; the WAN port does the same). Static entries: NUD_NOARP or NUD_PERMANENT, shown as "Static" with no age.
  • Age comes from NDA_CACHEINFO ndm_updated (the kernel ageing timer), verified MAC by MAC against brctl showmacs on a live router: 57 dynamic entries, all within 0.03 s. ndm_used is NOT used: it only moves on forwarding-path hits and reported ages of days for MACs that were seconds old. Sub-second ages round up to 1 s; the column is "Last seen".
  • Vendor lookup from a bundled OUI table (89 prefixes extracted from nmap-mac-prefixes); locally-administered MACs are flagged "Random" without a lookup.
  • UI: chassis-style table with search (MAC, port, vendor), i18n ES/EN, demo sample, gated to role=switch where clients is visible again (traffic stays hidden).

Field verification on a live router (rt3): 57 rows, 54 of them on the downlink port - matching the 54 non-local entries brctl showmacs reports there - ages 1-298 s under the bridge's 300 s ageing time.

Closes #520

A switch hides the Clients entry today (#518) because the router view is
built from DHCP leases and ARP, which a pure L2 device does not have. The
bridge FDB does know the useful part: which port each learned MAC sits on
and which VLANs it carries.

- internal/modules/l2clients.go reads the FDB over netlink AF_BRIDGE,
  skipping NTF_SELF entries and folding the VLANs of each (MAC, port)
  pair into one row. The `bridge` binary is never invoked: it is absent
  on some targets and busybox ip cannot dump the FDB.
- The L2 view keeps the Clients menu name but carries no DHCP semantics:
  no leases, reservations, blocking or quotas.
- GET /api/l2clients serves the probe. Shell renders the new page when
  the role is switch; Traffic stays hidden with its own reason.
- Vendor guess from a partial IEEE OUI table (extracted from the local
  nmap database, not typed from memory). Locally administered MACs are
  flagged as random instead of guessed.
- Demo stub, ES/EN strings and unit tests for the assembly logic, the OUI
  table consistency and the HTTP handler.
A netlink AF_BRIDGE dump filtered by the bridge's own index returns only
the bridge's local entries: on a live router (rt3) it came back with 8
entries and 0 learned ones, so the L2 inventory would have been empty on
every device. Filtering by each bridge port returns the learned entries,
60 of them on that same router, and the port of every entry is known by
construction instead of resolved from an index.

The read also drops a port's own address (hostapd publishes the BSSID MAC
as a static FDB entry and the WAN port does the same), reports a configured
entry as static for both kernel states it uses (NOARP and PERMANENT) and
normalises the VLAN list, because a filtered bridge reports the same MAC
once with VLAN 0 and once with its real VLAN.

Verified with the real probe on the live router: 55 rows, 54 of them on
the downlink port, matching the 54 non-local entries brctl showmacs
reports there.
ndm_used only moves when an entry is hit in the forwarding path, so it
reported ages of days for MACs that were seconds old. The kernel's
ageing timer is ndm_updated (NDA_CACHEINFO), verified MAC by MAC against
brctl showmacs on a live router: 57 dynamic entries, all within 0.03 s,
the sampling interval. The column now reads that field, shows no age for
a static entry (its timer is time since boot, not an age) and rounds a
sub-second age up to one second so the freshest rows no longer read
"unknown". The header is renamed to "Last seen", which is what the
value means.
@gnacho
gnacho merged commit 3406dec into main Oct 16, 2026
2 checks passed
@gnacho
gnacho deleted the feat/520-fdb-l2-clients branch October 16, 2026 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FDB-based client inventory for switch-role devices

1 participant