From 6c5489873bc66384bb9b4d8519721613a977d1ae Mon Sep 17 00:00:00 2001 From: Codescribe Date: Wed, 27 May 2026 03:21:58 -0400 Subject: [PATCH] fix(windows): include domainless VPN DNS adapters in discovery --- vpn_dns_windows.go | 53 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/vpn_dns_windows.go b/vpn_dns_windows.go index f5a76e1..e02cf15 100644 --- a/vpn_dns_windows.go +++ b/vpn_dns_windows.go @@ -32,28 +32,45 @@ func DiscoverVPNDNS(ctx context.Context) []VPNDNSConfig { var vpnConfigs []VPNDNSConfig for _, aa := range aas { - // Skip adapters that are not up if aa.OperStatus != winipcfg.IfOperStatusUp { - Log(ctx, logger.Debug(), "Skipping adapter %s - not up, status: %d", + Log(ctx, logger.Debug(), "Skipping adapter %s - not up, status: %d", aa.FriendlyName(), aa.OperStatus) continue } - // Skip software loopback if aa.IfType == winipcfg.IfTypeSoftwareLoopback { Log(ctx, logger.Debug(), "Skipping %s (software loopback)", aa.FriendlyName()) continue } - // INVERT the ValidInterfaces filter: we want non-physical/non-hardware adapters - // that are UP and have DNS servers AND DNS suffixes + // INVERT the ValidInterfaces filter: we want non-physical/non-hardware adapters. _, isValidPhysical := validInterfacesMap[aa.FriendlyName()] if isValidPhysical { Log(ctx, logger.Debug(), "Skipping %s (physical/hardware adapter)", aa.FriendlyName()) continue } - // Collect DNS servers + // Skip adapters that have no routable unicast addresses. An adapter + // with only link-local (fe80::) or APIPA (169.254.x.x) addresses is + // not actually connected — its DNS servers are stale. This prevents + // picking up e.g. Tailscale's adapter when the app is installed but + // disconnected (OperStatus reports Up but only APIPA addresses exist). + hasRoutableAddr := false + for a := aa.FirstUnicastAddress; a != nil; a = a.Next { + ip := a.Address.IP() + if ip == nil { + continue + } + if !ip.IsLinkLocalUnicast() { + hasRoutableAddr = true + break + } + } + if !hasRoutableAddr { + Log(ctx, logger.Debug(), "Skipping %s - no routable addresses (likely disconnected)", aa.FriendlyName()) + continue + } + var servers []string for dns := aa.FirstDNSServerAddress; dns != nil; dns = dns.Next { ip := dns.Address.IP() @@ -69,8 +86,18 @@ func DiscoverVPNDNS(ctx context.Context) []VPNDNSConfig { servers = append(servers, ipStr) } - // Collect DNS suffixes (search/match domains) + // Check adapter-specific (connection-specific) DNS suffix first, + // since we want to map per-adapter DNS servers to per-adapter suffixes. + // This is what most traditional VPNs set (F5, Cisco AnyConnect, GlobalProtect). var domains []string + if connSuffix := strings.TrimSpace(aa.DNSSuffix()); connSuffix != "" { + domains = append(domains, connSuffix) + Log(ctx, logger.Debug(), "Using connection-specific DNS suffix for %s: %s", + aa.FriendlyName(), connSuffix) + } + + // Then check supplemental DNS suffix list (used by Tailscale and + // VPN clients that register search domains via the DNS Client API). for suffix := aa.FirstDNSSuffix; suffix != nil; suffix = suffix.Next { domain := strings.TrimSpace(suffix.String()) if domain != "" { @@ -78,8 +105,10 @@ func DiscoverVPNDNS(ctx context.Context) []VPNDNSConfig { } } - // Only include interfaces that have BOTH DNS servers AND search domains - if len(servers) > 0 && len(domains) > 0 { + // Accept VPN adapters with DNS servers even without domains. + // Domain-less configs still provide useful DNS server IPs that + // can serve existing split-rules and OS resolver queries. + if len(servers) > 0 { config := VPNDNSConfig{ InterfaceName: aa.FriendlyName(), Servers: servers, @@ -91,11 +120,11 @@ func DiscoverVPNDNS(ctx context.Context) []VPNDNSConfig { Log(ctx, logger.Debug(), "Found VPN DNS config - Interface: %s, Servers: %v, Domains: %v", config.InterfaceName, config.Servers, config.Domains) } else { - Log(ctx, logger.Debug(), "Skipping %s - insufficient DNS config (servers: %d, domains: %d)", - aa.FriendlyName(), len(servers), len(domains)) + Log(ctx, logger.Debug(), "Skipping %s - no DNS servers found", + aa.FriendlyName()) } } Log(ctx, logger.Debug(), "VPN DNS discovery completed: found %d VPN interfaces", len(vpnConfigs)) return vpnConfigs -} \ No newline at end of file +}