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))
|
// 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:
|
// 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.
|
// 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.
|
// 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 {
|
type FastlyXFF struct {
|
||||||
IPv4 []string `json:"addresses"` // IPv4 CIDR ranges (e.g., "23.235.32.0/20")
|
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")
|
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.
|
// TrustedNets holds parsed network prefixes for efficient IP range checking.
|
||||||
@@ -138,15 +156,33 @@ func (xff *FastlyXFF) EchoTrustOption() ([]echo.TrustOption, error) {
|
|||||||
return ranges, nil
|
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 {
|
func (xff *FastlyXFF) isTrustedProxy(ip string) bool {
|
||||||
addr, err := netip.ParseAddr(ip)
|
addr, err := netip.ParseAddr(ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check all IPv4 and IPv6 ranges
|
// Check all IPv4 and IPv6 ranges (Fastly + additional)
|
||||||
for _, s := range append(xff.IPv4, xff.IPv6...) {
|
allRanges := append(append(xff.IPv4, xff.IPv6...), xff.extraCIDRs...)
|
||||||
|
for _, s := range allRanges {
|
||||||
_, cidr, err := net.ParseCIDR(s)
|
_, cidr, err := net.ParseCIDR(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
|
@@ -213,3 +213,144 @@ func TestGetRealIPWithoutMiddleware(t *testing.T) {
|
|||||||
t.Errorf("GetRealIP() = %s, expected %s", realIP, expected)
|
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