Files
parking_solution/wiki/concepts/wsl-dev-networking.md
T
julian 552d87d75b feat(devices): K200L printer driver — live cover/paper status from the J-Speed LAN board
The park-buzi printer is a K200L (Xprinter/ICS XP-K200L; its LAN board and USB
descriptor call it "POS-80"). Its board serves the Rongta's five-row status table
under /prt_status.htm — but the reply carries no HTTP status line or headers, which
node:http rejects, so the Rongta driver could never read it and the unit was filed
in July as "no status page → generic driver" (reachability only).

New `k200l` driver (printer-k200l.ts): prints through the generic ESC/POS device
(same bytes, TCP 9100 or usblp) and reads the page over a raw socket, tolerant of
both the headerless and a proper HTTP reply. Mapping mirrors the Rongta: board
unreachable → offline; page not understood → degraded, never ready; any fault →
degraded naming it; USB → reachability floor. The Rongta driver is untouched.
Tests replay the captured headerless page (devices suite 76). Live against the
lab unit: ready; with the cover open the board reports cover open, paper out,
off-line.

Wiki: new k200l-printer entity (names, network setup from factory
192.168.123.100, board quirks, status page, what it means for park-buzi — over
USB the app never saw cover/paper state at all), cross-links on the Rongta,
status-monitoring, USB-transport and WSL-networking pages (parking-net pinned to
eth1 while the LAN NIC is eth0), index.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
2026-09-09 12:34:59 +02:00

8.4 KiB

type, tags, sources, updated
type tags sources updated
reference
parking
dev-environment
networking
wsl
troubleshooting
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):

[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 <OwningProcess>); kill with taskkill.exe /PID <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 /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).

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