Fix GetLanIPv4 to skip Tailscale/VPN/APIPA addresses
Some checks failed
CI / build-and-test (push) Failing after 26s

On a Windows host with both Ethernet (10.0.0.123) and Tailscale (169.254.83.107 link-local), the original first-hit-wins picker returned the Tailscale address — useless for the headless-host + thin-client scenario the LAN-reachable mode is designed for.

New picker prefers physical NICs (Ethernet/GigabitEthernet/Wireless80211), skips Tunnel-typed virtuals, and ranks: physical-routable > virtual-routable > APIPA. Verified against this host: now returns 10.0.0.123 instead of 169.254.83.107.
This commit is contained in:
Zac Gaetano 2026-05-10 13:11:11 -04:00
parent 6d9407a61f
commit d9eb02a9af

View file

@ -349,25 +349,46 @@ public sealed class GlobalSettingsViewModel : ObservableObject
} }
/// <summary> /// <summary>
/// Best-effort first IPv4 address that isn't loopback. Returns null if /// Best-effort routable IPv4 address suitable for showing the operator a
/// no LAN interface is up. The first hit is good enough for the URL — /// "paste me into the thin client" URL. Skips:
/// operators with multi-NIC setups can manually substitute. /// • loopback interfaces (127.x)
/// • tunnel/virtual interfaces (NetworkInterfaceType.Tunnel — e.g. WSL,
/// Hyper-V, Tailscale, OpenVPN-style virtuals)
/// • APIPA/link-local addresses (169.254.x — assigned when DHCP fails;
/// a host with one of these AND a real DHCP lease should pick the lease)
/// Prefers Ethernet/Wi-Fi over everything else, then falls back to the
/// first non-link-local non-loopback IPv4. Returns null only if no
/// usable address exists at all.
/// </summary> /// </summary>
private static string? GetLanIPv4() private static string? GetLanIPv4()
{ {
try try
{ {
string? linkLocalFallback = null;
string? otherFallback = null;
foreach (var ni in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces()) foreach (var ni in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
{ {
if (ni.OperationalStatus != System.Net.NetworkInformation.OperationalStatus.Up) continue; if (ni.OperationalStatus != System.Net.NetworkInformation.OperationalStatus.Up) continue;
if (ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Loopback) continue; if (ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Loopback) continue;
if (ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Tunnel) continue;
var isPhysical =
ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Ethernet ||
ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.GigabitEthernet ||
ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Wireless80211;
foreach (var ua in ni.GetIPProperties().UnicastAddresses) foreach (var ua in ni.GetIPProperties().UnicastAddresses)
{ {
if (ua.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork if (ua.Address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) continue;
&& !System.Net.IPAddress.IsLoopback(ua.Address)) if (System.Net.IPAddress.IsLoopback(ua.Address)) continue;
return ua.Address.ToString(); var addr = ua.Address.ToString();
var isLinkLocal = addr.StartsWith("169.254.", StringComparison.Ordinal);
if (isPhysical && !isLinkLocal) return addr; // best
if (!isLinkLocal) otherFallback ??= addr; // routable but virtual NIC
if (isLinkLocal) linkLocalFallback ??= addr; // worst
} }
} }
return otherFallback ?? linkLocalFallback;
} }
catch { /* best-effort */ } catch { /* best-effort */ }
return null; return null;