From d9eb02a9af0d7bcb981fbacff581ea6df0f867c6 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 10 May 2026 13:11:11 -0400 Subject: [PATCH] Fix GetLanIPv4 to skip Tailscale/VPN/APIPA addresses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../ViewModels/GlobalSettingsViewModel.cs | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs index f1afabc..000d23d 100644 --- a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs +++ b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs @@ -349,25 +349,46 @@ public sealed class GlobalSettingsViewModel : ObservableObject } /// - /// Best-effort first IPv4 address that isn't loopback. Returns null if - /// no LAN interface is up. The first hit is good enough for the URL — - /// operators with multi-NIC setups can manually substitute. + /// Best-effort routable IPv4 address suitable for showing the operator a + /// "paste me into the thin client" URL. Skips: + /// • 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. /// private static string? GetLanIPv4() { try { + string? linkLocalFallback = null; + string? otherFallback = null; foreach (var ni in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces()) { 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.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) { - if (ua.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork - && !System.Net.IPAddress.IsLoopback(ua.Address)) - return ua.Address.ToString(); + if (ua.Address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) continue; + if (System.Net.IPAddress.IsLoopback(ua.Address)) continue; + 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 */ } return null;