2ab5a39a57
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.
102 lines
4.2 KiB
Bash
Executable File
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 "$@"
|