| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- 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
- }
|