Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
4.6 kB · 197 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198package aqhttp
import ( "encoding/json" "fmt" "io" "net" "net/http" "net/url" "sync" "time")
const ( TypeA = 1 // IPv4 TypeAAAA = 28 // IPv6)
type dnsRecord struct { ips []string expiresAt time.Time}
type DoHResolver struct { Server string Client *http.Client invalidRanges []*net.IPNet cache map[string]*dnsRecord mu sync.RWMutex}
func NewDoHResolver(server string) *DoHResolver { if server == "" { server = "https://1.1.1.1/dns-query" } ipv4Bogons := []string{ "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4", "255.255.255.255/32", }
ipv6Bogons := []string{ "::/128", // Unspecified "::1/128", // Loopback "100::/64", // Discard prefix "2001::/32", // TEREDO "2001:10::/28", // Deprecated (ORCHID) "2001:db8::/32", // Documentation "fc00::/7", // Unique local addresses (ULA) "fe80::/10", // Link-local "ff00::/8", // Multicast }
ranges := append(ipv4Bogons, ipv6Bogons...) var invalidRanges []*net.IPNet for _, cidr := range ranges { _, network, err := net.ParseCIDR(cidr) if err == nil { invalidRanges = append(invalidRanges, network) } }
return &DoHResolver{ Server: server, Client: &http.Client{ Timeout: 10 * time.Second, }, invalidRanges: invalidRanges, cache: make(map[string]*dnsRecord), }}
type DoHResponse struct { Status int `json:"Status"` Answer []struct { Name string `json:"name"` Type int `json:"type"` TTL int `json:"TTL"` Data string `json:"data"` } `json:"Answer"`}
func (r *DoHResolver) Resolve(domain string, recordType int) ([]string, error) { cacheKey := fmt.Sprintf("%s:%d", domain, recordType)
r.mu.RLock() if record, ok := r.cache[cacheKey]; ok { if time.Now().Before(record.expiresAt) { defer r.mu.RUnlock() return record.ips, nil } } r.mu.RUnlock()
reqURL := fmt.Sprintf("%s?name=%s&type=%d", r.Server, url.QueryEscape(domain), recordType)
req, err := http.NewRequest("GET", reqURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) }
req.Header.Set("Accept", "application/dns-json")
resp, err := r.Client.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute request: %w", err) } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) }
body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) }
var dohResp DoHResponse if err := json.Unmarshal(body, &dohResp); err != nil { return nil, fmt.Errorf("failed to parse JSON response: %w", err) }
var results []string var minTTL = 3600 for _, answer := range dohResp.Answer { if answer.Type == recordType { results = append(results, answer.Data) if answer.TTL < minTTL { minTTL = answer.TTL } } }
if len(results) > 0 { r.mu.Lock() r.cache[cacheKey] = &dnsRecord{ ips: results, expiresAt: time.Now().Add(time.Duration(minTTL) * time.Second), } r.mu.Unlock() }
return results, nil}
// check if the given IP address is within known invalid rangesfunc (r *DoHResolver) IsInvalidIP(ip string) bool { pip := net.ParseIP(ip) if pip == nil { return true // unparseable IPs are invalid } for _, nw := range r.invalidRanges { if nw.Contains(pip) { return true } } return false}
// validates a HTTPS URL and returns a safe IP address to use for the request.func (r *DoHResolver) ValidateAndGetIP(urlStr string) (string, *url.URL, error) { parsedURL, err := url.Parse(urlStr) if err != nil { return "", nil, fmt.Errorf("failed to parse URL: %w", err) }
if parsedURL.Scheme != "https" { return "", nil, fmt.Errorf("only HTTPS URLs are allowed, got: %s", parsedURL.Scheme) }
hostname := parsedURL.Hostname() if hostname == "" { return "", nil, fmt.Errorf("URL has no hostname") }
ipv4Addrs, err := r.Resolve(hostname, TypeA) if err == nil && len(ipv4Addrs) > 0 { for _, ip := range ipv4Addrs { if !r.IsInvalidIP(ip) { return ip, parsedURL, nil } } }
ipv6Addrs, err := r.Resolve(hostname, TypeAAAA) if err == nil && len(ipv6Addrs) > 0 { for _, ip := range ipv6Addrs { if !r.IsInvalidIP(ip) { return ip, parsedURL, nil } } }
return "", nil, fmt.Errorf("no valid IP addresses found for %s (all resolved to internal/bogon addresses)", hostname)}