#!/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 "$@"