Set forwarded headers from trusted proxies only

This commit is contained in:
Viktor Liu
2026-02-08 17:31:10 +08:00
parent 5190923c70
commit ed58659a01
12 changed files with 608 additions and 57 deletions

View File

@@ -2,6 +2,7 @@ package accesslog
import (
"context"
"net/netip"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
@@ -14,18 +15,24 @@ type gRPCClient interface {
SendAccessLog(ctx context.Context, in *proto.SendAccessLogRequest, opts ...grpc.CallOption) (*proto.SendAccessLogResponse, error)
}
// Logger sends access log entries to the management server via gRPC.
type Logger struct {
client gRPCClient
logger *log.Logger
client gRPCClient
logger *log.Logger
trustedProxies []netip.Prefix
}
func NewLogger(client gRPCClient, logger *log.Logger) *Logger {
// NewLogger creates a new access log Logger. The trustedProxies parameter
// configures which upstream proxy IP ranges are trusted for extracting
// the real client IP from X-Forwarded-For headers.
func NewLogger(client gRPCClient, logger *log.Logger, trustedProxies []netip.Prefix) *Logger {
if logger == nil {
logger = log.StandardLogger()
}
return &Logger{
client: client,
logger: logger,
client: client,
logger: logger,
trustedProxies: trustedProxies,
}
}

View File

@@ -24,14 +24,15 @@ func (l *Logger) Middleware(next http.Handler) http.Handler {
status: http.StatusOK,
}
// Get the source IP before passing the request on as the proxy will modify
// headers that we wish to use to gather that information on the request.
sourceIp := extractSourceIP(r)
// Resolve the source IP using trusted proxy configuration before passing
// the request on, as the proxy will modify forwarding headers.
sourceIp := extractSourceIP(r, l.trustedProxies)
// Create a mutable struct to capture data from downstream handlers.
// We pass a pointer in the context - the pointer itself flows down immutably,
// but the struct it points to can be mutated by inner handlers.
capturedData := &proxy.CapturedData{RequestID: requestID}
capturedData.SetClientIP(sourceIp)
ctx := proxy.WithCapturedData(r.Context(), capturedData)
start := time.Now()

View File

@@ -1,43 +1,16 @@
package accesslog
import (
"net"
"net/http"
"slices"
"strings"
"net/netip"
"github.com/netbirdio/netbird/proxy/internal/proxy"
)
// requestIP attempts to extract the source IP from a request.
// Adapted from https://husobee.github.io/golang/ip-address/2015/12/17/remote-ip-go.html
// with the addition of some newer stdlib functions that are now
// available.
// The concept here is to look backwards through IP headers until
// the first public IP address is found. The hypothesis is that
// even if there are multiple IP addresses specified in these headers,
// the last public IP should be the hop immediately before reaching
// the server and therefore represents the "true" source IP regardless
// of the number of intermediate proxies or network hops.
func extractSourceIP(r *http.Request) string {
for _, h := range []string{"X-Forwarded-For", "X-Real-IP"} {
addresses := strings.Split(r.Header.Get(h), ",")
// Iterate from right to left until we get a public address
// that should be the address right before our proxy.
for _, address := range slices.Backward(addresses) {
// Trim the address because sometimes clients put whitespace in there.
ip := strings.TrimSpace(address)
// Parse the IP so that we can easily check whether it is a valid public address.
realIP := net.ParseIP(ip)
if !realIP.IsGlobalUnicast() || realIP.IsPrivate() || realIP.IsLoopback() {
continue
}
return ip
}
}
// Fallback to the requests RemoteAddr, this is least likely to be correct but
// should at least yield something in the event that the above has failed.
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
ip = r.RemoteAddr
}
return ip
// extractSourceIP resolves the real client IP from the request using trusted
// proxy configuration. When trustedProxies is non-empty and the direct
// connection is from a trusted source, it walks X-Forwarded-For right-to-left
// skipping trusted IPs. Otherwise it returns RemoteAddr directly.
func extractSourceIP(r *http.Request, trustedProxies []netip.Prefix) string {
return proxy.ResolveClientIP(r.RemoteAddr, r.Header.Get("X-Forwarded-For"), trustedProxies)
}