128
internal/clouddetect/detect.go
Normal file
128
internal/clouddetect/detect.go
Normal file
@@ -0,0 +1,128 @@
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user