Files
headscale/hscontrol/realip.go
Kristoffer Dalby 4cca63155d all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.

Pattern applied across the tree:
  In-package         [Foo], [Foo.Bar]
  Cross-package      [pkg.Foo], [pkg.Foo.Bar]
  Stdlib             [netip.Prefix], [errors.Is], [context.Context]
  Tailscale          [tailcfg.MapResponse], [tailcfg.Node.CapMap],
                     [tailcfg.NodeAttrSuggestExitNode]

Skip rules:
  - File:line refs left as plain text
  - HuJSON wire keys inside backtick raw strings untouched
  - ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
    symbols, left as plain text
  - JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
    link tags, decorative dividers — all left as-is
2026-05-19 09:55:22 +02:00

125 lines
3.0 KiB
Go

package hscontrol
import (
"fmt"
"net"
"net/http"
"net/netip"
realclientip "github.com/realclientip/realclientip-go"
)
const (
headerTrueClientIP = "True-Client-IP"
headerXRealIP = "X-Real-IP"
headerXForwardedFor = "X-Forwarded-For"
)
var proxyHeaders = [...]string{headerTrueClientIP, headerXRealIP, headerXForwardedFor}
// trustedProxyRealIP rewrites r.RemoteAddr from proxy headers when the
// peer is in trusted; for any other peer the headers are stripped so a
// downstream handler cannot read a spoofed value. X-Forwarded-For uses
// [realclientip.RightmostTrustedRangeStrategy] so prepending a value cannot win in a
// proxy chain.
func trustedProxyRealIP(trusted []netip.Prefix) (func(http.Handler) http.Handler, error) {
ranges := make([]net.IPNet, 0, len(trusted))
for _, p := range trusted {
ranges = append(ranges, prefixToIPNet(p))
}
trueClientIP, err := realclientip.NewSingleIPHeaderStrategy(headerTrueClientIP)
if err != nil {
return nil, fmt.Errorf("%s strategy: %w", headerTrueClientIP, err)
}
xRealIP, err := realclientip.NewSingleIPHeaderStrategy(headerXRealIP)
if err != nil {
return nil, fmt.Errorf("%s strategy: %w", headerXRealIP, err)
}
xForwardedFor, err := realclientip.NewRightmostTrustedRangeStrategy(headerXForwardedFor, ranges)
if err != nil {
return nil, fmt.Errorf("%s strategy: %w", headerXForwardedFor, err)
}
strategy := realclientip.NewChainStrategy(trueClientIP, xRealIP, xForwardedFor)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !peerTrusted(r.RemoteAddr, trusted) {
for _, h := range proxyHeaders {
r.Header.Del(h)
}
next.ServeHTTP(w, r)
return
}
// Proxy headers carry no port; write the IP alone so
// `remote=` logs the resolved client, not the proxy's
// ephemeral TCP port.
if ip := strategy.ClientIP(r.Header, r.RemoteAddr); ip != "" {
r.RemoteAddr = ip
}
next.ServeHTTP(w, r)
})
}, nil
}
// peerTrusted returns false on unparseable input so callers fall
// through to the header-stripping path.
func peerTrusted(remoteAddr string, trusted []netip.Prefix) bool {
addr, ok := parsePeerAddr(remoteAddr)
if !ok {
return false
}
for _, p := range trusted {
if p.Contains(addr) {
return true
}
}
return false
}
func parsePeerAddr(remoteAddr string) (netip.Addr, bool) {
if remoteAddr == "" {
return netip.Addr{}, false
}
ap, err := netip.ParseAddrPort(remoteAddr)
if err == nil {
return ap.Addr(), true
}
host, _, splitErr := net.SplitHostPort(remoteAddr)
if splitErr != nil {
host = remoteAddr
}
addr, err := netip.ParseAddr(host)
if err != nil {
return netip.Addr{}, false
}
return addr, true
}
// prefixToIPNet bridges to realclientip-go, which predates net/netip.
func prefixToIPNet(p netip.Prefix) net.IPNet {
addr := p.Addr()
if addr.Is4() {
b := addr.As4()
return net.IPNet{IP: b[:], Mask: net.CIDRMask(p.Bits(), 32)}
}
b := addr.As16()
return net.IPNet{IP: b[:], Mask: net.CIDRMask(p.Bits(), 128)}
}