feat(xff): add AddTrustedCIDR for custom proxies
- Add AddTrustedCIDR() method to support non-Fastly proxies - Enable trusting custom CIDR ranges (e.g., 10.0.0.0/8) - Validate CIDR format before adding to trusted list - Maintain backward compatibility with Fastly-only usage Allows mixed proxy environments where requests pass through both Fastly CDN and custom internal proxies/load balancers. Uses precise CIDR terminology instead of generic "range".
This commit is contained in:
@@ -47,6 +47,23 @@
|
||||
//
|
||||
// http.ListenAndServe(":8080", middleware(handler))
|
||||
//
|
||||
// # Net/HTTP with Additional Trusted Ranges
|
||||
//
|
||||
// fastlyRanges, err := fastlyxff.New("fastly.json")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// // Add custom trusted CIDRs (e.g., internal load balancers)
|
||||
// // Note: For Echo framework, use the ekko package for additional ranges
|
||||
// err = fastlyRanges.AddTrustedCIDR("10.0.0.0/8")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// middleware := fastlyRanges.HTTPMiddleware()
|
||||
// handler := middleware(yourHandler)
|
||||
//
|
||||
// The JSON file typically contains IP ranges in this format:
|
||||
//
|
||||
// {
|
||||
@@ -69,10 +86,11 @@ import (
|
||||
|
||||
// FastlyXFF represents Fastly's published IP ranges for their CDN edge servers.
|
||||
// This structure matches the JSON format provided by Fastly for their public IP ranges.
|
||||
// It contains separate lists for IPv4 and IPv6 CIDR ranges.
|
||||
// It contains separate lists for IPv4 and IPv6 CIDR ranges, plus additional trusted CIDRs.
|
||||
type FastlyXFF struct {
|
||||
IPv4 []string `json:"addresses"` // IPv4 CIDR ranges (e.g., "23.235.32.0/20")
|
||||
IPv6 []string `json:"ipv6_addresses"` // IPv6 CIDR ranges (e.g., "2a04:4e40::/32")
|
||||
IPv4 []string `json:"addresses"` // IPv4 CIDR ranges (e.g., "23.235.32.0/20")
|
||||
IPv6 []string `json:"ipv6_addresses"` // IPv6 CIDR ranges (e.g., "2a04:4e40::/32")
|
||||
extraCIDRs []string // Additional trusted CIDRs added via AddTrustedCIDR
|
||||
}
|
||||
|
||||
// TrustedNets holds parsed network prefixes for efficient IP range checking.
|
||||
@@ -138,15 +156,33 @@ func (xff *FastlyXFF) EchoTrustOption() ([]echo.TrustOption, error) {
|
||||
return ranges, nil
|
||||
}
|
||||
|
||||
// isTrustedProxy checks if the given IP address belongs to Fastly's trusted IP ranges.
|
||||
// AddTrustedCIDR adds an additional CIDR to the list of trusted proxies.
|
||||
// This allows trusting proxies beyond Fastly's published ranges.
|
||||
// The cidr parameter must be a valid CIDR notation (e.g., "10.0.0.0/8", "192.168.1.0/24").
|
||||
// Returns an error if the CIDR format is invalid.
|
||||
func (xff *FastlyXFF) AddTrustedCIDR(cidr string) error {
|
||||
// Validate CIDR format
|
||||
_, _, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add to extra CIDRs
|
||||
xff.extraCIDRs = append(xff.extraCIDRs, cidr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// isTrustedProxy checks if the given IP address belongs to Fastly's trusted IP ranges
|
||||
// or any additional CIDRs added via AddTrustedCIDR.
|
||||
func (xff *FastlyXFF) isTrustedProxy(ip string) bool {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check all IPv4 and IPv6 ranges
|
||||
for _, s := range append(xff.IPv4, xff.IPv6...) {
|
||||
// Check all IPv4 and IPv6 ranges (Fastly + additional)
|
||||
allRanges := append(append(xff.IPv4, xff.IPv6...), xff.extraCIDRs...)
|
||||
for _, s := range allRanges {
|
||||
_, cidr, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
continue
|
||||
|
@@ -213,3 +213,144 @@ func TestGetRealIPWithoutMiddleware(t *testing.T) {
|
||||
t.Errorf("GetRealIP() = %s, expected %s", realIP, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTrustedCIDR(t *testing.T) {
|
||||
xff := &FastlyXFF{
|
||||
IPv4: []string{"192.0.2.0/24"},
|
||||
IPv6: []string{"2001:db8::/32"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cidr string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid IPv4 range", "10.0.0.0/8", false},
|
||||
{"valid IPv6 range", "fc00::/7", false},
|
||||
{"valid single IP", "203.0.113.1/32", false},
|
||||
{"invalid CIDR", "not-a-cidr", true},
|
||||
{"invalid format", "10.0.0.0/99", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := xff.AddTrustedCIDR(tt.cidr)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AddTrustedCIDR(%s) error = %v, wantErr %v", tt.cidr, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomTrustedCIDRs(t *testing.T) {
|
||||
xff := &FastlyXFF{
|
||||
IPv4: []string{"192.0.2.0/24"},
|
||||
IPv6: []string{"2001:db8::/32"},
|
||||
}
|
||||
|
||||
// Add custom trusted CIDRs
|
||||
err := xff.AddTrustedCIDR("10.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add trusted CIDR: %v", err)
|
||||
}
|
||||
|
||||
err = xff.AddTrustedCIDR("172.16.0.0/12")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add trusted CIDR: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
ip string
|
||||
expected bool
|
||||
}{
|
||||
// Original Fastly ranges
|
||||
{"192.0.2.1", true},
|
||||
{"2001:db8::1", true},
|
||||
// Custom CIDRs
|
||||
{"10.1.2.3", true},
|
||||
{"172.16.1.1", true},
|
||||
// Not trusted
|
||||
{"198.51.100.1", false},
|
||||
{"172.15.1.1", false},
|
||||
{"10.0.0.0", true}, // Network address should still match
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.ip, func(t *testing.T) {
|
||||
result := xff.isTrustedProxy(tt.ip)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isTrustedProxy(%s) = %v, expected %v", tt.ip, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPMiddlewareWithCustomCIDRs(t *testing.T) {
|
||||
xff := &FastlyXFF{
|
||||
IPv4: []string{"192.0.2.0/24"},
|
||||
IPv6: []string{"2001:db8::/32"},
|
||||
}
|
||||
|
||||
// Add custom trusted CIDR for internal proxies
|
||||
err := xff.AddTrustedCIDR("10.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add trusted CIDR: %v", err)
|
||||
}
|
||||
|
||||
middleware := xff.HTTPMiddleware()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteAddr string
|
||||
xForwardedFor string
|
||||
expectedRealIP string
|
||||
}{
|
||||
{
|
||||
name: "custom trusted proxy with XFF",
|
||||
remoteAddr: "10.1.2.3:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "fastly proxy with XFF",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
{
|
||||
name: "untrusted proxy ignored",
|
||||
remoteAddr: "172.16.1.1:80",
|
||||
xForwardedFor: "198.51.100.1",
|
||||
expectedRealIP: "172.16.1.1",
|
||||
},
|
||||
{
|
||||
name: "chain through custom and fastly",
|
||||
remoteAddr: "192.0.2.1:80",
|
||||
xForwardedFor: "198.51.100.1, 10.1.2.3",
|
||||
expectedRealIP: "198.51.100.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var capturedIP string
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedIP = GetRealIP(r)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = tt.remoteAddr
|
||||
if tt.xForwardedFor != "" {
|
||||
req.Header.Set("X-Forwarded-For", tt.xForwardedFor)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
middleware(handler).ServeHTTP(rr, req)
|
||||
|
||||
if capturedIP != tt.expectedRealIP {
|
||||
t.Errorf("expected real IP %s, got %s", tt.expectedRealIP, capturedIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user