Files
nmon/internal/auth/proxy.go

134 lines
3.4 KiB
Go

package auth
import (
"context"
"fmt"
"net"
"net/http"
"net/netip"
"strings"
)
// Proxies is the set of reverse proxies whose X-Forwarded-* headers nmon believes. A request from any
// other peer has those headers ignored (and removed), because anyone can send them.
type Proxies struct{ nets []netip.Prefix }
// ParseProxies parses CIDRs or single addresses ("10.0.0.0/8", "192.0.2.7", "::1").
func ParseProxies(cidrs []string) (*Proxies, error) {
p := &Proxies{}
for _, c := range cidrs {
c = strings.TrimSpace(c)
if pf, err := netip.ParsePrefix(c); err == nil {
p.nets = append(p.nets, pf.Masked())
continue
}
a, err := netip.ParseAddr(c)
if err != nil {
return nil, fmt.Errorf("trusted_proxy %q: not a CIDR or address", c)
}
p.nets = append(p.nets, netip.PrefixFrom(a.Unmap(), a.Unmap().BitLen()))
}
return p, nil
}
func (p *Proxies) trusts(a netip.Addr) bool {
if p == nil {
return false
}
a = a.Unmap()
for _, n := range p.nets {
if n.Contains(a) {
return true
}
}
return false
}
// Client is what a request says about its origin after proxy headers have been judged.
type Client struct {
// IP is the original client: the direct peer, or the nearest untrusted hop in X-Forwarded-For when
// the peer is a trusted proxy.
IP netip.Addr
Scheme string
Host string
// ViaProxy is true when the headers were honoured.
ViaProxy bool
}
// Resolve returns the Client for r.
func (p *Proxies) Resolve(r *http.Request) Client {
c := Client{Scheme: "http", Host: r.Host}
if r.TLS != nil {
c.Scheme = "https"
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
}
peer, err := netip.ParseAddr(host)
if err != nil {
return c
}
c.IP = peer.Unmap()
if !p.trusts(peer) {
return c
}
c.ViaProxy = true
c.IP = p.originalClient(peer, r.Header.Values("X-Forwarded-For"))
if v := firstToken(r.Header.Get("X-Forwarded-Proto")); v == "https" || v == "http" {
c.Scheme = v
}
if v := firstToken(r.Header.Get("X-Forwarded-Host")); v != "" {
c.Host = v
}
return c
}
// originalClient walks X-Forwarded-For from the right, skipping hops that are themselves trusted
// proxies; the first other address is the client. Entries to its left were supplied by it and are not believed.
func (p *Proxies) originalClient(peer netip.Addr, xff []string) netip.Addr {
var hops []string
for _, v := range xff {
for _, h := range strings.Split(v, ",") {
hops = append(hops, strings.TrimSpace(h))
}
}
client := peer.Unmap()
for i := len(hops) - 1; i >= 0; i-- {
a, err := netip.ParseAddr(hops[i])
if err != nil {
return client
}
client = a.Unmap()
if !p.trusts(a) {
return client
}
}
return client
}
func firstToken(v string) string {
first, _, _ := strings.Cut(v, ",")
return strings.ToLower(strings.TrimSpace(first))
}
// scrub removes the forwarding headers from a request whose peer is not a trusted proxy, so no later
// handler can be fooled by them.
func scrub(r *http.Request) {
for _, h := range []string{"X-Forwarded-For", "X-Forwarded-Proto", "X-Forwarded-Host", "X-Forwarded-Port", "Forwarded"} {
r.Header.Del(h)
}
}
type clientKey struct{}
// ClientFromContext returns the Client Middleware resolved for the request.
func ClientFromContext(ctx context.Context) (Client, bool) {
c, ok := ctx.Value(clientKey{}).(Client)
return c, ok
}
func withClient(ctx context.Context, c Client) context.Context {
return context.WithValue(ctx, clientKey{}, c)
}