--- type: reference tags: [parking, dev-environment, networking, wsl, troubleshooting] sources: [] updated: 2026-09-09 --- # WSL2 Dev Networking (for device testing) > Dev-environment note, not product architecture. Recorded because reaching real > hardware (the [[uhppote-controller]]) from a dev box running under **WSL2** took > significant debugging. If you test devices from WSL, read this first. ## The problem By default WSL2 uses **NAT networking**: the Linux VM sits on its own virtual subnet (e.g. `172.x`), not the Windows host's LAN. Consequences for device work: - **UDP broadcast (UHPPOTE discovery) cannot leave the VM** — a `get-devices` broadcast gets `EACCES` / never reaches a controller on the physical LAN. The device is reachable from *Windows* but not from *inside WSL*. - Even unicast to a LAN device may not route, depending on setup. ## The fix: mirrored networking Switch WSL to **mirrored** mode so it shares the Windows host's interfaces (and thus the real LAN). Requires **Windows 11 22H2+** and **WSL ≥ 2.0**. `%UserProfile%\.wslconfig` (create it; it doesn't exist by default): ```ini [wsl2] networkingMode=mirrored firewall=false # Windows Firewall otherwise filters WSL traffic (can drop UDP replies) [experimental] hostAddressLoopback=true # host <-> WSL over the host's IP ``` Apply: in **PowerShell** `wsl --shutdown`, wait ~10 s, reopen WSL. Verify with `ip -4 addr` — interfaces should now show the **real LAN subnet** (e.g. `10.0.10.x`) instead of `172.x`. (Microsoft recommends editing via the **WSL Settings** GUI rather than the file by hand.) > `wsl --shutdown` kills the dev servers — restart `pnpm dev` afterward. ## After mirrored mode: app-level gotchas that remained Mirrored networking is necessary but **not sufficient** — these still bit us: - **Multiple interfaces.** Mirrored WSL exposes *all* host NICs (LAN, Tailscale/CGNAT `100.x`, docker bridges). UHPPOTE discovery must broadcast on **every** subnet, not the first one — see [[device-discovery]]. - **Subnet-directed broadcast** (`10.0.10.255`, not `255.255.255.255`) — the lib won't enable `SO_BROADCAST` otherwise. See [[device-discovery]]. - **`localhost` → IPv6 first.** `localhost` resolves to `::1`, but the backend binds IPv4 (`127.0.0.1`). Node's Vite proxy can stall on the v6 attempt before falling back — point the proxy at `127.0.0.1` explicitly. (See [[local-dev-workflow]].) - **Windows-side listeners collide with WSL binds — INVISIBLY (2026-07-13).** Under mirrored mode, a process listening on the WINDOWS side makes the same port `EADDRINUSE` inside WSL, but it never appears in Linux `ss`/`lsof` — the port looks free yet won't bind. Bit us as "tauri dev: Could not connect to http://localhost:5173 after 180s": a DIFFERENT React app's dev server running on the Windows side held `::1:5173`, so the WSL Vite silently auto-incremented to 5174 while Tauri's `devUrl` is the FIXED string `http://localhost:5173` in `tauri.conf.json` (it cannot follow the auto-increment). Diagnose from WSL with `powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort 5173 -State Listen"` (then `Get-Process -Id `); kill with `taskkill.exe /PID /F`. Guard: `strictPort: true` in the web `vite.config` so the mismatch fails in a second with a clear error instead of a 3-minute hang on the wrong port. ## Multi-subnet source-address trap (the "ARP works but ping/TCP dies" bug) Field devices arrive **statically configured on assorted `/24`s** by whoever installed them last (e.g. a camera on `10.0.10.121`, a printer on `10.0.10.6`, others on `192.168.1.x`). The host copes by carrying **one IP per device subnet on a single NIC** (this is correct — you do **not** need a NIC per subnet). But stacking subnets on one interface exposes a Linux source-selection trap: - Connected routes come up as `proto kernel scope link` **with no preferred source**. With two such subnets on one NIC, the kernel may pick the **wrong source address** — e.g. sourcing traffic to `10.0.10.121` from `192.168.1.123`. - Symptom is baffling: **ARP resolves and the neighbor shows `REACHABLE`** (L2 is fine, source address is irrelevant to ARP) while **every ping and TCP connect times out** (replies have a wrong/unroutable source → dropped, possibly by uRPF). Looks like "the device is down / the whole subnet is unreachable" when nothing is actually broken. - **Diagnose:** `ip route get ` shows the chosen `src` — if it's an address on a *different* subnet, that's the bug. Confirm by forcing the right source: `ping -I ` (or `curl --interface …`) — instant replies. - **Fix (runtime):** pin the preferred source on the connected route, per subnet: `sudo ip route replace /24 dev proto kernel scope link src metric ` (use `replace`, not `change` — `change` errors `RTNETLINK: No such file` if the route isn't up yet). Do **not** delete the other subnet's address unless it's genuinely unwanted — you need all of them to reach all the devices. - **Fix (permanent, this box):** `deploy/wsl-fix-route-source.sh` + `deploy/parking-net.service`. The script walks each `proto kernel scope link` route on the NIC and pins `src` to THIS host's own address in that same subnet — **no hardcoded IPs**, so it also covers future device subnets; it's idempotent, preserves the route metric, and tolerates a missing route. The systemd unit (oneshot, `enabled`) reapplies it on every WSL boot — which is the point, since `wsl --shutdown` otherwise wipes the runtime fix (mirrored mode re-clones the Windows addresses fresh each boot, see below). Install once: copy the unit to `/etc/systemd/system/`, `systemctl enable --now parking-net`. Gotchas hit while building it: `network.target` is too early for mirrored-mode addresses (the script waits up to 15s for a route to appear); and it must NOT `set -e` or one failed `ip` call aborts the whole boot fixer. > **Root cause is on the Windows side.** Mirrored mode clones the Windows host NIC's addresses into > Linux at every boot, so the stray `192.168.1.x` lives on Windows — the truly permanent fix is to > remove/reconfigure it there (or set `SkipAsSource`/interface metric). The systemd hook is the > self-contained Linux-side answer that needs no Windows changes. Verified on hardware (2026-06-15): after the hook, `10.0.10.121` pings and the real [[lpr-camera]] Hikvision driver pulls a snapshot with **no** source-forcing (`localAddress` becomes optional). > **It bit again, 2026-09-09 — and the fix was pinned to the wrong NIC.** Configuring the > [[k200l-printer]] meant adding `192.168.123.101` beside `10.0.10.203` on the mirrored LAN NIC; > WSL then sourced 10.0.10.x traffic from the 192.168.123 address: `ping` fine, every HTTP > connect timing out, `ip route get 10.0.10.7` showing `src 192.168.123.101`. `parking-net.service` > was active but pins **`eth1`**, and the mirrored LAN NIC is **`eth0`** on this box now — so the > boot fixer was a no-op. Run `deploy/wsl-fix-route-source.sh eth0` (and fix the unit's argument), > or `curl --interface 10.0.10.203 …` as a one-off. Interface names are not stable across WSL > reboots/NIC changes; the script accepts the NIC as an argument for exactly this reason. ## On the real appliance: multi-subnet is a deployment config, not a WSL hack Production is a **dedicated hardened Linux appliance** ([[disk-os-hardening]]), so the WSL story above is dev-only. The device-subnet problem persists, though, and is solved the same way at the OS level: the appliance NIC carries **one address per device subnet**, each connected route with a pinned `src`, made persistent (systemd-networkd / netplan). Per the threat model this still rides on **[[network-isolation]]** — device subnets are isolated segments reachable only by the host. The long-term clean answer is to **re-IP the devices onto one planned parking-system subnet** at install so the host needs only one address; the multi-subnet config is what you run until then. ## Alternative if you can't use mirrored mode Windows 10 / old WSL can't do mirrored mode. Options: run the **backend natively on Windows** (shares the LAN), or use **unicast by IP** instead of broadcast discovery (target the controller's known IP — the driver supports an explicit host). On the real **appliance** (a dedicated hardened Linux box, [[disk-os-hardening]]) none of this applies — it's bare-metal on the device VLAN ([[network-isolation]]).