hscontrol: gate proxy header trust on trusted_proxies
chi middleware.RealIP was mounted unconditionally on both the
public router and the noise router, so any client could send
X-Real-IP or X-Forwarded-For and have the spoofed value land in
r.RemoteAddr and the access-log remote= field.
Add a top-level trusted_proxies config option (list of CIDRs) and
replace middleware.RealIP with a gated middleware that:
- honours True-Client-IP / X-Real-IP / X-Forwarded-For only when
r.RemoteAddr is inside one of the configured prefixes;
- strips those three headers from every request whose peer is
not trusted, so downstream handlers cannot read them.
X-Forwarded-For is parsed via realclientip-go's
RightmostTrustedRangeStrategy so a prepended value cannot win in a
proxy chain. trustedProxies() rejects 0.0.0.0/0 and ::/0 at config
load.
Empty trusted_proxies (the default) skips the mount entirely;
r.RemoteAddr is the directly-connecting TCP peer.
This commit is contained in:
parent
1f48ebb376
commit
c6c29c05e5
6 changed files with 511 additions and 2 deletions
124
hscontrol/realip.go
Normal file
124
hscontrol/realip.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
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
|
||||
// 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)}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue