package clouddetect import ( "context" "fmt" "net" "net/http" "strings" "time" ) // Result represents the outcome of a cloud proxy detection. type Result struct { IsProxy bool `json:"is_proxy"` Provider string `json:"provider,omitempty"` Details string `json:"details,omitempty"` } // knownCIDRs maps providers to a slice of CIDR strings that represent their IP ranges. // In a production implementation these would be loaded from data files; for this prototype // a small representative subset is hard‑coded. var knownCIDRs = map[string][]string{ "Cloudflare": { "103.21.244.0/22", "104.16.0.0/12", "2606:4700::/32", }, "AWS CloudFront": { "13.32.0.0/15", "13.54.0.0/15", "2600:9000::/28", }, "Azure Front Door": { "40.112.0.0/13", "52.239.0.0/16", "2603:1030::/32", }, } // Detect attempts to determine whether the target is behind a known cloud proxy. // It performs DNS resolution, checks the resolved IPs against known CIDR ranges, // and inspects HTTP response headers for provider signatures. func Detect(target string) (*Result, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Resolve IP addresses for the target. // Strip any port component if present (e.g., "example.com:443"). host := target if strings.Contains(target, ":") { if h, _, err := net.SplitHostPort(target); err == nil { host = h } } var ips []net.IP ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host) if err != nil { // DNS lookup failed; continue without IPs. ips = nil } // Duplicate error handling removed – the previous block already handles DNS failures. // Prepare CIDR parsers. parsedCIDRs := make(map[string][]*net.IPNet) for prov, cidrs := range knownCIDRs { for _, cidr := range cidrs { _, ipnet, err := net.ParseCIDR(cidr) if err != nil { continue // skip malformed entries } parsedCIDRs[prov] = append(parsedCIDRs[prov], ipnet) } } // Load additional CIDR ranges from external file (cloud_ranges.txt). if extCIDRs, err := LoadCIDRs("internal/clouddetect/cloud_ranges.txt"); err == nil { for prov, nets := range extCIDRs { parsedCIDRs[prov] = append(parsedCIDRs[prov], nets...) } } // Check each resolved IP against the CIDR tables. for _, ip := range ips { for prov, nets := range parsedCIDRs { for _, net := range nets { if net.Contains(ip) { return &Result{ IsProxy: true, Provider: prov, Details: fmt.Sprintf("IP %s matches %s range", ip, prov), }, nil } } } } // If no CIDR match, attempt an HTTP HEAD request to look for provider headers. // Use the original target (host) for the request to avoid port issues. url := fmt.Sprintf("http://%s", host) req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) if err == nil { client := &http.Client{ Timeout: 5 * time.Second, } resp, err := client.Do(req) if err == nil { defer resp.Body.Close() serverHeader := resp.Header.Get("Server") viaHeader := resp.Header.Get("Via") // Simple heuristics based on common header values. if strings.Contains(strings.ToLower(serverHeader), "cloudflare") || strings.Contains(strings.ToLower(viaHeader), "cloudflare") { return &Result{IsProxy: true, Provider: "Cloudflare", Details: "Detected via HTTP headers"}, nil } if strings.Contains(strings.ToLower(serverHeader), "amazon") || strings.Contains(strings.ToLower(viaHeader), "aws") { return &Result{IsProxy: true, Provider: "AWS CloudFront", Details: "Detected via HTTP headers"}, nil } if strings.Contains(strings.ToLower(serverHeader), "azure") || strings.Contains(strings.ToLower(viaHeader), "azure") { return &Result{IsProxy: true, Provider: "Azure Front Door", Details: "Detected via HTTP headers"}, nil } } } // No proxy indicators found. return &Result{IsProxy: false}, nil }