Files
parking_solution/wiki/concepts/wsl-dev-networking.md
julian 2ab5a39a57 Permanent WSL2 dev fix for multi-subnet source-address trap
Mirrored mode re-clones the Windows NIC's addresses each boot, so the kernel
keeps picking the wrong source for stacked device subnets (10.0.10.x sourced
from 192.168.1.123) — ARP resolves but ping/TCP dies, and every runtime
ip-route fix is wiped by wsl --shutdown.

deploy/wsl-fix-route-source.sh pins each scope-link route's src to this
host's own address in that subnet (no hardcoded IPs, idempotent, preserves
metric, non-fatal per route, waits for the route at boot). deploy/parking-net
.service reapplies it on every boot.

Dev-box only; the appliance is bare-metal Linux with static networkd config.
Verified: camera pings with no -I flag; driver pulls a snapshot with no
localAddress set.
2026-06-15 16:17:59 +02:00

6.8 KiB

type, tags, sources, updated
type tags sources updated
reference
parking
dev-environment
networking
wsl
troubleshooting
2026-06-15

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):

[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.)

Multi-subnet source-address trap (the "ARP works but ping/TCP dies" bug)

Field devices arrive statically configured on assorted /24s 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 <device-ip> 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 <correct-src> <device-ip> (or curl --interface <correct-src> …) — instant replies.

  • Fix (runtime): pin the preferred source on the connected route, per subnet: sudo ip route replace <subnet>/24 dev <nic> proto kernel scope link src <correct-host-ip> metric <m> (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).

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).