package ike import ( "context" "encoding/json" "errors" "fmt" "net" "net/http" "net/url" "strings" "time" ) const googleDNSOverHTTPS = "https://dns.google/resolve" // A small number of operators publish the standard ePDG CNAME globally but // return its A records only when the recursive DNS query appears to originate // in the home country. Keep this list deliberately narrow: ordinary ePDGs must // continue to use the host resolver, and a fallback is attempted only after // that resolver has failed. var geoRestrictedEPDGSubnets = map[string]string{ "epdg.epc.mnc002.mcc262.pub.3gppnetwork.org": "109.192.0.0/24", // Vodafone Germany } type dnsOverHTTPSResponse struct { Status int `json:"Status"` Answer []struct { Type int `json:"type"` Data string `json:"data"` } `json:"Answer"` } func resolveEPDG(ctx context.Context, resolver *net.Resolver, host string) ([]net.IPAddr, error) { if resolver == nil { resolver = net.DefaultResolver } addresses, systemErr := resolver.LookupIPAddr(ctx, host) if systemErr == nil && len(addresses) > 0 { return addresses, nil } normalized := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(host), ".")) subnet := geoRestrictedEPDGSubnets[normalized] if subnet == "" { if systemErr != nil { return nil, systemErr } return nil, errors.New("ePDG did not resolve to an IP address") } client := &http.Client{Timeout: 8 * time.Second} var fallbackErr error // Vodafone's authoritative response has a 60-second TTL and recursive // resolvers can briefly cache the global CNAME without its geo-restricted // address records. Stay inside the runtime's two-minute setup window and // wait through one complete negative-cache TTL so a single reconnect is // sufficient; users should not have to click Reconnect repeatedly. const fallbackAttempts = 13 for attempt := 0; attempt < fallbackAttempts; attempt++ { var fallback []net.IPAddr fallback, fallbackErr = resolveEPDGWithECS(ctx, client, googleDNSOverHTTPS, normalized, subnet) if fallbackErr == nil && len(fallback) > 0 { return fallback, nil } if attempt+1 < fallbackAttempts { select { case <-time.After(5 * time.Second): case <-ctx.Done(): return nil, ctx.Err() } } } if systemErr == nil { systemErr = errors.New("system DNS returned no IP addresses") } return nil, fmt.Errorf("system DNS failed (%v); geographic DNS fallback failed: %w", systemErr, fallbackErr) } func resolveEPDGWithECS( ctx context.Context, client *http.Client, endpoint, host, subnet string, ) ([]net.IPAddr, error) { if client == nil { return nil, errors.New("nil DNS-over-HTTPS client") } parsed, err := url.Parse(endpoint) if err != nil { return nil, fmt.Errorf("parse DNS-over-HTTPS endpoint: %w", err) } query := parsed.Query() query.Set("name", strings.TrimSpace(host)) query.Set("type", "A") query.Set("edns_client_subnet", strings.TrimSpace(subnet)) parsed.RawQuery = query.Encode() request, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil) if err != nil { return nil, fmt.Errorf("build DNS-over-HTTPS request: %w", err) } request.Header.Set("Accept", "application/dns-json") request.Header.Set("Cache-Control", "no-cache") response, err := client.Do(request) if err != nil { return nil, fmt.Errorf("query DNS-over-HTTPS: %w", err) } defer response.Body.Close() if response.StatusCode != http.StatusOK { return nil, fmt.Errorf("DNS-over-HTTPS returned HTTP %d", response.StatusCode) } var payload dnsOverHTTPSResponse decoder := json.NewDecoder(response.Body) if err := decoder.Decode(&payload); err != nil { return nil, fmt.Errorf("decode DNS-over-HTTPS response: %w", err) } if payload.Status != 0 { return nil, fmt.Errorf("DNS-over-HTTPS returned DNS status %d", payload.Status) } result := make([]net.IPAddr, 0, len(payload.Answer)) for _, answer := range payload.Answer { if answer.Type != 1 && answer.Type != 28 { continue } ip := net.ParseIP(strings.TrimSuffix(strings.TrimSpace(answer.Data), ".")) if ip == nil { continue } duplicate := false for _, existing := range result { if existing.IP.Equal(ip) { duplicate = true break } } if !duplicate { result = append(result, net.IPAddr{IP: append(net.IP(nil), ip...)}) } } if len(result) == 0 { return nil, errors.New("DNS-over-HTTPS response contained no ePDG IP addresses") } return result, nil }