mirror of
https://github.com/fleetdm/fleet
synced 2026-05-21 07:58:31 +00:00
86 lines
3.2 KiB
Go
86 lines
3.2 KiB
Go
|
|
package endpointer
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/realclientip/realclientip-go"
|
||
|
|
)
|
||
|
|
|
||
|
|
// NewClientIPStrategy creates a ClientIPStrategy based on the trusted_proxies configuration.
|
||
|
|
//
|
||
|
|
// Config values:
|
||
|
|
// - "" (empty): Legacy behavior for backwards compatibility - trusts True-Client-IP,
|
||
|
|
// X-Real-IP, and leftmost X-Forwarded-For. This is deprecated; use "none" when
|
||
|
|
// exposing the server directly to the internet.
|
||
|
|
// - "none": Ignores all headers, uses only RemoteAddr.
|
||
|
|
// - A header name prefixed with `header:` (e.g., "header:True-Client-IP"):
|
||
|
|
// Trust this single-IP header, fall back to RemoteAddr.
|
||
|
|
// - A number (e.g., "2"): Trust X-Forwarded-For with this many proxy hops
|
||
|
|
// - Comma-separated IPs/CIDRs (e.g., "10.0.0.0/8,192.168.0.0/16"):
|
||
|
|
// Trust X-Forwarded-For from requests originating from these proxy ranges.
|
||
|
|
func NewClientIPStrategy(trustedProxies string) (realclientip.Strategy, error) {
|
||
|
|
trustedProxies = strings.TrimSpace(trustedProxies)
|
||
|
|
|
||
|
|
var strategy realclientip.Strategy
|
||
|
|
var err error
|
||
|
|
|
||
|
|
if trustedProxies == "" {
|
||
|
|
// Empty: legacy behavior for backwards compatibility.
|
||
|
|
return &legacyStrategy{}, nil
|
||
|
|
} else if strings.EqualFold(trustedProxies, "none") {
|
||
|
|
// "none": Trust no one; return (non-spoofable) RemoteAddr only.
|
||
|
|
return realclientip.RemoteAddrStrategy{}, nil
|
||
|
|
} else if headerName, ok := strings.CutPrefix(trustedProxies, "header:"); ok {
|
||
|
|
// Check if the value is a single IP header name.
|
||
|
|
strategy, err = realclientip.NewSingleIPHeaderStrategy(headerName)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("invalid header name %q: %w", trustedProxies, err)
|
||
|
|
}
|
||
|
|
} else if hopCount, err := strconv.Atoi(trustedProxies); err == nil {
|
||
|
|
// Check if it's a number (hop count).
|
||
|
|
if hopCount < 1 {
|
||
|
|
return nil, fmt.Errorf("trusted_proxies hop count must be >= 1, got %d", hopCount)
|
||
|
|
}
|
||
|
|
strategy, err = realclientip.NewRightmostTrustedCountStrategy("X-Forwarded-For", hopCount)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to create hop count strategy: %w", err)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// Otherwise, parse as comma-separated IP ranges.
|
||
|
|
rangeStrs := strings.Split(trustedProxies, ",")
|
||
|
|
for i := range rangeStrs {
|
||
|
|
rangeStrs[i] = strings.TrimSpace(rangeStrs[i])
|
||
|
|
}
|
||
|
|
|
||
|
|
trustedRanges, err := realclientip.AddressesAndRangesToIPNets(rangeStrs...)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("invalid trusted_proxies IP ranges: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
strategy, err = realclientip.NewRightmostTrustedRangeStrategy("X-Forwarded-For", trustedRanges)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to create IP range strategy: %w", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Chain strategy with RemoteAddr as fallback.
|
||
|
|
return realclientip.NewChainStrategy(strategy, realclientip.RemoteAddrStrategy{}), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// legacyStrategy implements the original ExtractIP behavior for backwards compatibility.
|
||
|
|
// This is deprecated; if your server is exposed directly to the internet, switch to
|
||
|
|
// the "none" strategy.
|
||
|
|
type legacyStrategy struct{}
|
||
|
|
|
||
|
|
func (s *legacyStrategy) ClientIP(headers http.Header, remoteAddr string) string {
|
||
|
|
// Build a minimal http.Request to pass to extractIP
|
||
|
|
r := &http.Request{
|
||
|
|
Header: headers,
|
||
|
|
RemoteAddr: remoteAddr,
|
||
|
|
}
|
||
|
|
return extractIP(r)
|
||
|
|
}
|