diff --git a/WORKAROUNDS.md b/WORKAROUNDS.md index 0a332e91..aa68440e 100644 --- a/WORKAROUNDS.md +++ b/WORKAROUNDS.md @@ -71,6 +71,7 @@ A known limitation with no fix yet. | WA-025 | brcmfmac 4-way-handshake offload authentication — upstream kernel/wpa_supplicant, no code in this repo | Since hostap commit 416386060, wpa_supplicant 2.11+ expects `NL80211_CMD_PORT_AUTHORIZED` from drivers that offload the 4-way handshake; brcmfmac never reports it, so authentication intermittently fails on Apple Silicon (BRCM4378/4387/4388). nixpkgs already carries the Broadcom revert in wpa_supplicant 2.12; the proper fix, Janne Grunau's `wifi: brcmfmac: cfg80211: Report port_authorized for 4-way HS offload` (2026-09), is still under review. Entering the key again after such a failure only creates a duplicate profile, so cancel and retry instead | Fix lands via a nixpkgs kernel update (or by carrying the patch through `nixpkgs-patched`) | 2026-10-07 | on every kernel bump | active | | WA-033 | USB-C/XHCI port `_PLD` ACPI noise — this board's DSDT, no code in this repo | every boot logs 16 `ACPI Error: Aborting method \_SB.PC00.TXHC.RHUB.SS01._PLD ... (AE_NOT_FOUND)` lines, plus `\_SB.PC02.XHCI.RHUB.HS01._PLD`; the ports work, and these messages are unrelated to the GPU's ATPX failure, which names `\_SB.PC00.GFX0.ATPX` instead | Drop if a BIOS update defines the `_PLD` descriptors | 2026-10-14 | on a BIOS update | active | | WA-034 | IPv6-first resolution is dead on the wired link — `hosts/idols-aquamarine/default.nix` (`networking.getaddrinfo.precedence`) | the proxy gateway answers DNS in fake-IP mode and its IPv6 path is dead: an AAAA lookup returns a fake address whose connection fails at once (measured: `curl -6` fails in 8 ms, `curl -4` answers in 0.7 s). glibc clients are covered by the RFC 6724 table with the IPv4-mapped row raised; nix's libcurl and other non-glibc resolvers ignore `/etc/gai.conf`. Disabling IPv6 with `boot.kernel.sysctl` did not work here — networkd re-enables it on the wired link for `DHCP = "ipv6"`/`IPv6AcceptRA` — so that setting was dropped on 2026-10-14 | Drop once the gateway serves proxied names over IPv6 as well (the adjacent fix is `dns.ipv6: false` in mihomo, see WA-030) | 2026-10-14 | on a mihomo or gateway change | active | +| WA-035 | SSH retry + multiplexing to LAN peers, ARP fallback tuning — `home/base/tui/ssh.nix`, `hosts/idols-ai/neighbour-probe.nix` | the desktop reaches the LAN over a Mesh WiFi backhaul, which drops unicast to a peer whose MAC it has aged out of its forwarding table while still flooding broadcast: a capture shows seven SYNs from one socket addressed to the cached MAC with no answer, three unicast ARP probes unanswered, then a broadcast ARP answered in 0.4 ms after ~12 s of failed connects. The ssh settings retry a short connect and keep a master warm so the entry stays fresh; the neighbour-probe tunings walk the kernel to that broadcast fallback in ~1 s | Drop once the backhaul is wired or the mesh stops dropping unknown unicast | 2026-10-16 | when the backhaul changes | active | ## WIP diff --git a/home/base/tui/ssh.nix b/home/base/tui/ssh.nix index 9f687856..bc07981c 100644 --- a/home/base/tui/ssh.nix +++ b/home/base/tui/ssh.nix @@ -3,8 +3,26 @@ lib, pkgs, mysecrets, + myvars, ... }: +let + # WA-035: settings for the home LAN, which the desktop reaches over a Mesh + # WiFi backhaul. + lanHost = { + # A lost first SYN must not fail the connection; a LAN handshake is ~2 ms, + # so the connect is cut short and retried instead of hanging. + ConnectTimeout = 1; + ConnectionAttempts = 3; + # One persistent master per host keeps a session (and with it the mesh's + # forwarding entry for that peer) open, so later connections reuse it + # instead of resolving and handshaking again. Three missed keepalives reap a + # master whose path died. + ControlMaster = "auto"; + ControlPersist = "30m"; + ServerAliveInterval = 15; + }; +in { home.file.".ssh/romantic.pub".source = "${mysecrets}/public/romantic.pub"; @@ -12,20 +30,23 @@ enable = true; enableDefaultConfig = false; - settings."*" = { - ForwardAgent = false; - AddKeysToAgent = "yes"; - Compression = true; - ServerAliveInterval = 0; - ServerAliveCountMax = 3; - HashKnownHosts = false; - UserKnownHostsFile = "~/.ssh/known_hosts"; - ControlMaster = "no"; - ControlPath = "~/.ssh/master-%r@%n:%p"; - ControlPersist = "no"; - }; - + # Keep this one definition: Nix merges a `settings.""` attribute + # path into `settings = ...` only while that value stays an attrset literal, + # so the generated LAN patterns are merged inside it with `//` instead. settings = { + "*" = { + ForwardAgent = false; + AddKeysToAgent = "yes"; + Compression = true; + ServerAliveInterval = 0; + ServerAliveCountMax = 3; + HashKnownHosts = false; + UserKnownHostsFile = "~/.ssh/known_hosts"; + ControlMaster = "no"; + ControlPath = "~/.ssh/master-%r@%n:%p"; + ControlPersist = "no"; + }; + "github.com" = { # WA-028: pinned to IPv4 until the mihomo v6 fake-ip pool is proven here AddressFamily = "inet"; @@ -37,8 +58,10 @@ "192.168.*" = { ForwardAgent = true; - }; - }; + } + // lanHost; + } + // lib.genAttrs (builtins.attrNames myvars.networking.hostsAddr) (_: lanHost); }; # Keep ~/.ssh/config a real, user-owned file rather than a /nix/store symlink. diff --git a/hosts/idols-ai/default.nix b/hosts/idols-ai/default.nix index c448c283..ce75754b 100644 --- a/hosts/idols-ai/default.nix +++ b/hosts/idols-ai/default.nix @@ -35,6 +35,9 @@ in ./restic.nix + # WA-035: neighbour-probe fallback for the Mesh WiFi backhaul to the homelab. + ./neighbour-probe.nix + # others ./ai ]; diff --git a/hosts/idols-ai/neighbour-probe.nix b/hosts/idols-ai/neighbour-probe.nix new file mode 100644 index 00000000..e9c843ad --- /dev/null +++ b/hosts/idols-ai/neighbour-probe.nix @@ -0,0 +1,35 @@ +{ + config, + myvars, + ... +}: +let + # The wired link to the router, i.e. the one that leaves through the Mesh WiFi + # backhaul to the homelab. Taken from `hostsAddr` so a NIC rename cannot leave + # these tunings pointing at a table that no longer exists. + iface = myvars.networking.hostsAddr.${config.networking.hostName}.iface; +in +{ + # WA-035: the mesh backhaul drops unicast to a peer whose MAC it has aged out + # while still flooding broadcast, so a connection to an idle peer waits for the + # kernel to give up on unicast ARP and fall back to broadcast. Walk that + # neighbour state machine in ~1s instead of ~10s, which fits inside ssh's 5s + # retry window (see `home/base/tui/ssh.nix` for the other half). + # + # Cost: while a peer is genuinely down, probes go out 5x faster for the same + # bounded count -- one unicast probe, then the unchanged broadcast attempts. The + # failed-neighbour re-probe pace (`interval_probe_time_ms`) is deliberately left + # alone, so a peer that stays down is not probed more often than before. + # + # `default` and the link's own table are both set: a device copies `default` + # when it is registered, so a later write to `default` does not reach a table + # that already exists. + boot.kernel.sysctl = { + "net.ipv4.neigh.default.delay_first_probe_time" = 1; # revalidate after 1s, not 5s + "net.ipv4.neigh.default.retrans_time_ms" = 200; # probe every 200ms, not 1s + "net.ipv4.neigh.default.ucast_solicit" = 1; # one unicast probe, then broadcast + "net.ipv4.neigh.${iface}.delay_first_probe_time" = 1; + "net.ipv4.neigh.${iface}.retrans_time_ms" = 200; + "net.ipv4.neigh.${iface}.ucast_solicit" = 1; + }; +}