Files
parking_solution/deploy/wsl-fix-route-source.sh
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

102 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# WSL2 mirrored-mode source-address fix (dev box only).
#
# Problem: in WSL2 mirrored networking the Windows host's interfaces — and ALL
# their IPs — are cloned into Linux on every boot. When two device subnets land
# on one NIC (e.g. 192.168.1.x AND 10.0.10.x on eth1), the kernel's connected
# routes come up `scope link` with NO preferred source, and source selection can
# pick the WRONG address (sourcing 10.0.10.x traffic from 192.168.1.123). ARP
# still resolves (L2), so the device looks REACHABLE while every ping/TCP times
# out. See wiki/concepts/wsl-dev-networking.md.
#
# Fix: for each connected `scope link` route, pin its preferred `src` to THIS
# host's own address in that same subnet. No hardcoded IPs — derived at runtime,
# so it also covers future device subnets. Idempotent; a no-op when nothing needs
# fixing. Runs at boot via parking-net.service.
#
# Production note: the real appliance is bare-metal Linux, not WSL — there this
# is just static networkd/netplan config. This script exists only for the dev box.
# NB: intentionally NOT `set -e`. This is a best-effort boot fixer; an individual
# `ip` call failing (e.g. a route not up yet) must not abort the rest.
set -uo pipefail
fix_iface() {
local iface="$1"
# Each connected /N route on this iface that the kernel manages (proto kernel,
# scope link) — i.e. the directly-attached subnets. Capture the full line so we
# can preserve attributes (notably `metric`) when we replace the route.
ip -4 route show dev "$iface" proto kernel scope link | while read -r line; do
local subnet="${line%% *}" # e.g. "10.0.10.0/24"
local prefix="${subnet%/*}"
# Preserve a metric if the route has one (mirrored-mode routes carry e.g. 281);
# replacing without it would change the route's priority.
local metric=""
case "$line" in *" metric "*) metric="metric ${line##* metric }";; esac
# Find THIS host's own address inside the same subnet — the correct src.
local hostip=""
local cidr
for cidr in $(ip -4 -o addr show dev "$iface" | awk '{print $4}'); do
if ipcalc_net "$cidr" "$subnet"; then hostip="${cidr%/*}"; break; fi
done
[ -n "$hostip" ] || continue
local current
current=$(ip -4 route get "$prefix" 2>/dev/null | sed -n 's/.*src \([0-9.]*\).*/\1/p' | head -1)
[ "$current" = "$hostip" ] && continue # already correct — no-op
# `replace` creates-or-updates, so it works whether or not the route is
# present yet (avoids the boot-race RTNETLINK "No such file" that `change` hits).
# Non-fatal: a single failure must not abort the whole boot fixer.
if ip route replace "$subnet" dev "$iface" proto kernel scope link src "$hostip" $metric; then
echo "pinned $subnet -> src $hostip (was ${current:-none})"
else
echo "warn: could not pin $subnet -> src $hostip" >&2
fi
done
return 0
}
# True if address $1 (a.b.c.d/p) is inside subnet $2 (n.n.n.0/p), same prefix len.
ipcalc_net() {
local addr="${1%/*}" alen="${1#*/}"
local net="${2%/*}" nlen="${2#*/}"
[ "$alen" = "$nlen" ] || return 1
# Compare the network part by masking both to /nlen.
local a n
a=$(mask_to_net "$addr" "$nlen")
n=$(mask_to_net "$net" "$nlen")
[ "$a" = "$n" ]
}
# Mask an IPv4 dotted-quad to its /len network address.
mask_to_net() {
local ip="$1" len="$2"
local IFS=. ; read -r o1 o2 o3 o4 <<<"$ip"
local int=$(( (o1<<24) + (o2<<16) + (o3<<8) + o4 ))
local mask=$(( len == 0 ? 0 : (0xFFFFFFFF << (32 - len)) & 0xFFFFFFFF ))
local net=$(( int & mask ))
echo "$(( (net>>24)&255 )).$(( (net>>16)&255 )).$(( (net>>8)&255 )).$(( net&255 ))"
}
main() {
# Default to eth1 (the mirrored LAN NIC here); accept overrides as args.
local ifaces=("${@:-eth1}")
# Boot race: WSL mirrored mode can populate the interface's addresses/routes a
# beat after the unit starts. Wait (bounded) for at least one connected route
# to appear on the first interface before pinning.
local i tries=0
for i in "${ifaces[@]}"; do
while [ "$tries" -lt 15 ] \
&& [ -z "$(ip -4 route show dev "$i" proto kernel scope link 2>/dev/null)" ]; do
sleep 1; tries=$((tries + 1))
done
break
done
for i in "${ifaces[@]}"; do
ip link show "$i" >/dev/null 2>&1 && fix_iface "$i"
done
}
main "$@"