Skip to content

fix(net): hot plugging -- a cable or a Wi-Fi network that goes and comes back - #792

Merged
pdfinn merged 6 commits into
masterfrom
fix/net-hotplug
Oct 8, 2026
Merged

pdfinn merged 6 commits into
masterfrom
fix/net-hotplug

Conversation

@pdfinn

@pdfinn pdfinn commented Oct 8, 2026

Copy link
Copy Markdown
Member

On the bench Pi, a cable pulled and put back never came back. Neither interface followed the network coming and going.

Fixes

  • Wired (etherusb). The link was read once and the driver exited, so a cable pulled kept its address on a dead port, and a cable plugged in later was never configured.
    • The driver now stays and polls the link every 2 s: BMSR on the LAN78xx, OID_GEN_MEDIA_CONNECT_STATUS on RNDIS. It opens the exclusive control endpoint per poll, so a re-run for the same adapter is not refused.
    • Link down: the address goes.
    • Link up: DHCP again, then the address and default route come back. No answer means it retries at the next poll, and it never invents an address.
    • The boot-time "no carrier" check covers RNDIS too.
  • Routes (os/ip/ipifc.c). A route keeps its resolved interface while that interface's ifcid is unchanged, and ifcid changed only on bind and unbind.
    • Wired and Wi-Fi share their network's route (Requals), so after the wired address went, that network's traffic kept going out of the dead port. Wi-Fi went down with the cable.
    • ipifcremlifc now bumps the ifcid of the interface that lost the address.
    • Bumping every interface's broke loopback connections. The harness's console checks over 127.0.0.1 failed 3/3 that way, and passed 280/0 with that version removed.
  • DHCP (appl/lib/dhcpclient.b). The lease watchdog held UDP 68 for the whole lease, so the other interface's DHCP failed with address in use.
    • It now closes the conversation while it waits, and reopens it to renew, rebind, rediscover or release.
  • Wi-Fi (osinit). Before, it made three tries to join at boot and then never again.
    • Not joined yet: the last supplicant keeps running (it re-joins by itself), and the address is asked for when it gets there.
    • After a re-join (a new "group key" in the supplicant's log): the old address is dropped and DHCP asked again.

Evidence

On the board (Pi 3B+, this build), physical tests:

test result
cable pulled, replugged Wi-Fi stayed reachable throughout; wired back 6 s after the plug
boot with no cable, then plug in no carrier; ... until a link comes up, Wi-Fi up; wired configured 3 s after the plug, no reboot
carried out of Wi-Fi range and back re-joined on its own about 2 min after return (supplicant log: link lost, re-associated, group key)
cable back after running on Wi-Fi alone wired answered within seconds

Not observed on the board:

  • The Wi-Fi DHCP renewal after a re-join: the console was disconnected during the walk.
  • A boot with no Wi-Fi in range.

QEMU:

  • tests/host/qemu/usbnet-link-status.patch lets QEMU's usb-net report its link, and the harness pulls and restores the link through QMP.
  • bcm2837 280/0 (hot-plug checks and the adapter re-run checks included).
  • virt + bcm2711 157/0 on a6db0e0. Since then, only the narrowed ipifcremlifc change.
  • Hosted dhcp_test 2/2 under -c0 and -c1, including its lease-renewal case.

The 48 h soak of this build is running on the board.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FsRUgdQ797dzH667L9i6HK

pdfinn and others added 5 commits October 8, 2026 10:20
…rk that goes and comes back

Found on the bench Pi: a cable pulled and put back never came back, and
the board was off its wired network until a reboot. Neither interface
followed the network coming and going.

Wired (etherusb). The link was read once, at start, and never again,
and the driver exited after configuring the interface. A cable pulled
left its address on a port that could not carry it -- with the radio on
the same subnet the stack kept sending that subnet out of the dead port
-- and a cable put back, or plugged in after a boot without one, was
never configured. The driver now stays, keeps its control endpoint and
polls the link every 2 s (the PHY's BMSR on a LAN78xx, the adapter's
OID_GEN_MEDIA_CONNECT_STATUS on RNDIS). Down: the address goes, and its
subnet with it, so another interface carries the traffic. Up: DHCP
again, the address and the default route back, wired preferred; no
answer is asked again at the next poll, never an invented address. A
failed control transfer means the adapter itself has gone, and the
watcher ends, releasing the endpoint for the next run. The boot-time
"no carrier" check now asks an RNDIS adapter too.

Wi-Fi (osinit). Three tries to join at boot, then never again: a board
that booted away from its network, or before its router was up, had no
Wi-Fi until a reboot. The last supplicant is left running -- it re-joins
by itself, at a widening interval -- and the address is asked for when
it gets there. After a re-join (a new "group key" in the supplicant's
log) the old address is dropped and DHCP asked again: the address was
asked for once and went stale.

QEMU's usb-net never reported its link (media_state fixed at
connected), so tests/host/qemu/usbnet-link-status.patch gives it a
link_status_changed, and the harness switches the link off and on
through QMP and checks the address goes and comes back. The Wi-Fi half
has no QEMU model; it needs the board.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FsRUgdQ797dzH667L9i6HK
…een exchanges; one run per adapter

Found by pulling the bench Pi's cable on the hot-plug build:

- Wi-Fi went off the network with the cable, and came back only when
  the cable did. A route keeps the interface it last resolved to while
  that interface's ifcid is unchanged, and ifcid changed only on bind
  and unbind. The wired and radio interfaces share their network's route
  (refcounted, Requals), so when the wired one gave up its address the
  route kept sending the network's traffic out of the dead port.
  ipifcstale() bumps every interface's ifcid when an address is added
  or removed, so each route resolves again to an interface that has the
  address now -- including back to the wired one when it returns.

- The cable came back and its DHCP failed, "udp setup failed: address
  in use", every two seconds. The client port is the machine's, and the
  radio's lease watchdog held it for the whole lease to renew. It now
  closes the conversation while it waits and opens it again to renew,
  rebind, rediscover or release (opensession).

- The harness found a second etherusb run for the same adapter refused:
  the link watcher held the exclusive control endpoint. It opens it for
  each poll now, and ends when it cannot have it or when its interface
  is no longer bound to /net/ether0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FsRUgdQ797dzH667L9i6HK
Bumping every interface's ifcid on any address change (ipifcstale) made
every cached route resolve again, loopback's included, and the bench
harness's network-console checks over 127.0.0.1 failed three runs in
three; with it disabled they passed (280/0). What the cable pull needs
is narrower: the routes cached on the interface that gave up its
address. ipifcremlifc bumps that interface's ifcid alone, and adding an
address bumps nothing -- a returning wired port gets its default route
back from etherusb, which re-adding resets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FsRUgdQ797dzH667L9i6HK
…ISC-V

The link watcher was etherusb's, for USB adapters (the Pi 3B+'s
LAN78xx). A board whose Ethernet is a kernel driver -- GENET on the
Pi 4, virtio-net on virt and riscvvirt, GEM on PolarFire -- is
configured by etherusb -k once at boot, and its link was never watched.

- etherusb -k starts the same watcher, reading the link from the netif
  ("link: N" in /net/ether0/stats), which GENET and GEM already keep
  from their PHYs. Down: the address goes. Up: DHCP again, the address
  and the default route back.
- virtio-net set nif.link to 1 once and never changed it. It now
  negotiates VIRTIO_NET_F_STATUS and keeps nif.link from the device's
  status, read at start and on a configuration-change interrupt.

The harness checks it on virt, where QEMU's virtio-net reports
set_link without a patch: the address goes when the link does and
comes back, with its default route, when the link does. GENET has no
QEMU model; on a Pi 4 it wants the board.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FsRUgdQ797dzH667L9i6HK
@pdfinn

pdfinn commented Oct 8, 2026 •

Copy link
Copy Markdown
Member Author

Added in 2d061d3: hot plugging over a kernel link driver, so this isn't only the Pi 3B+'s.

board wired Ethernet before now
Pi 3B+ LAN78xx over USB (etherusb) watched (earlier commits) watched
Pi 4 GENET, kernel configured once, never watched watched (GENET already kept nif.link from its PHY)
virt, riscvvirt virtio-net, kernel configured once; driver always said "up" watched; virtio-net now reports the device's link (VIRTIO_NET_F_STATUS)
PolarFire GEM, kernel configured once, never watched watched (GEM already kept nif.link)

etherusb -k runs the same watcher, reading link: N from /net/ether0/stats.

The fixes from the earlier commits are in shared code, so they apply to every board: routes follow an address that goes (os/ip), and DHCP frees port 68 between exchanges. The Wi-Fi changes (osinit) apply to every board with the radio (Pi 3B+, Pi 4).

QEMU:

  • virt + bcm2711 159/0, including the new virt hot-plug checks: set_link off removes the address; on brings it back with its default route.
  • bcm2837 278/0 (one transient "no QMP" skip in the mouse check, which passed on the previous run of the same kernel code).
  • riscvvirt/mpfs: covered by CI's RISC-V suite.
  • GENET has no QEMU model, so wired hot plug on a Pi 4 needs the board.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FsRUgdQ797dzH667L9i6HK

@pdfinn
pdfinn merged commit 94a77b2 into master Oct 8, 2026
29 checks passed
@pdfinn
pdfinn deleted the fix/net-hotplug branch October 8, 2026 09:10
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.

1 participant