up
Some checks failed
Go CI / test (push) Has been cancelled

This commit is contained in:
Dev
2025-09-13 12:30:01 +03:00
commit 4d51c65060
14 changed files with 1227 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
# Cloud provider IP ranges (partial list for demonstration)
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

View 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 hardcoded.
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
}

View File

@@ -0,0 +1,47 @@
package clouddetect
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// TestDetect_HeaderBasedDetection verifies that Detect can identify a Cloudflare proxy
// by inspecting the Server header returned from an HTTP HEAD request.
func TestDetect_HeaderBasedDetection(t *testing.T) {
// Create a test server that mimics a Cloudflareprotected endpoint.
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "cloudflare")
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
// Extract host without scheme (e.g., "127.0.0.1:XXXXX").
host := strings.TrimPrefix(ts.URL, "http://")
// Run detection against the test server.
res, err := Detect(host)
if err != nil {
t.Fatalf("Detect returned error: %v", err)
}
if !res.IsProxy {
t.Fatalf("Expected proxy detection, got IsProxy=false")
}
if res.Provider != "Cloudflare" {
t.Fatalf("Expected provider Cloudflare, got %s", res.Provider)
}
}
// TestDetect_NoProxy ensures that a target without known CIDR or header signatures
// is reported as not being behind a proxy.
func TestDetect_NoProxy(t *testing.T) {
// Use localhost where no special headers are set.
res, err := Detect("localhost")
if err != nil {
t.Fatalf("Detect returned error: %v", err)
}
if res.IsProxy {
t.Fatalf("Expected no proxy detection, got IsProxy=true")
}
}

View File

@@ -0,0 +1,18 @@
package clouddetect
import (
"context"
"time"
)
// GracefulShutdown handles context cancellation and cleanup for detection operations.
func GracefulShutdown(ctx context.Context) {
// Placeholder for future shutdown logic.
// Currently, simply wait for context cancellation or a timeout.
select {
case <-ctx.Done():
// Context cancelled.
case <-time.After(5 * time.Second):
// Timeout reached.
}
}

View File

@@ -0,0 +1,50 @@
package clouddetect
import (
"bufio"
"net"
"os"
"strings"
)
// LoadCIDRs reads the cloud_ranges.txt file and parses the CIDR blocks for each provider.
// The file format is simple: a provider name followed by a colon, then one CIDR per line.
// Blank lines and lines starting with '#' are ignored.
func LoadCIDRs(filePath string) (map[string][]*net.IPNet, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
cidrs := make(map[string][]*net.IPNet)
var currentProvider string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Provider header, e.g., "Cloudflare:"
if strings.HasSuffix(line, ":") {
currentProvider = strings.TrimSuffix(line, ":")
continue
}
// CIDR entry
if currentProvider == "" {
// Skip CIDR without a provider header.
continue
}
_, ipnet, err := net.ParseCIDR(line)
if err != nil {
// Invalid CIDR skip it.
continue
}
cidrs[currentProvider] = append(cidrs[currentProvider], ipnet)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return cidrs, nil
}