Files
llmgw/internal/clientip/clientip.go
Ilker ManapandClaude Opus 5.5 fe5b7f88f0 Believe X-Forwarded-* only from router.trusted_proxies
A new internal/clientip says who sent a request and how: the connection's
address, scheme and Host, or through a configured proxy what it forwarded.
The base URL in pages and setup scripts, isHTTPS for cookies, and the
per-address limits of auth and the sign-in forms use it. /setup scripts are
no-store.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 10:29:02 +02:00

123 lines
2.9 KiB
Go

// Package clientip says who sent a request and how, believing a proxy's
// X-Forwarded-* headers only when the request came from a proxy the
// configuration names.
//
// Believed from anyone, X-Forwarded-Host decides the address written into the
// setup scripts clients pipe to a shell, and X-Forwarded-For lets one client
// claim to be any number of others to whatever counts attempts per address.
// Believed from nobody, every client behind a proxy is one address, and a
// limit per address is a limit on all of them together.
package clientip
import (
"net"
"net/http"
"net/netip"
"strings"
"sync/atomic"
)
var trusted atomic.Pointer[[]netip.Prefix]
// SetTrusted names the proxies whose forwarding headers are believed:
// addresses or CIDR prefixes. Anything unparsable is skipped and returned.
func SetTrusted(list []string) (skipped []string) {
var out []netip.Prefix
for _, s := range list {
s = strings.TrimSpace(s)
if p, err := netip.ParsePrefix(s); err == nil {
out = append(out, p.Masked())
continue
}
if a, err := netip.ParseAddr(s); err == nil {
out = append(out, netip.PrefixFrom(a, a.BitLen()))
continue
}
skipped = append(skipped, s)
}
trusted.Store(&out)
return skipped
}
func isTrusted(addr string) bool {
a, err := netip.ParseAddr(addr)
if err != nil {
return false
}
a = a.Unmap()
list := trusted.Load()
if list == nil {
return false
}
for _, p := range *list {
if p.Contains(a) {
return true
}
}
return false
}
// peer is the address the connection came from.
func peer(r *http.Request) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}
// fromProxy reports whether the request came through a trusted proxy.
func fromProxy(r *http.Request) bool { return isTrusted(peer(r)) }
// Addr is the client's address: the connection's, or through trusted
// proxies the nearest address in X-Forwarded-For that is not one of them.
func Addr(r *http.Request) string {
p := peer(r)
if !isTrusted(p) {
return p
}
hops := strings.Split(r.Header.Get("X-Forwarded-For"), ",")
for i := len(hops) - 1; i >= 0; i-- {
h := strings.TrimSpace(hops[i])
if h == "" {
continue
}
if !isTrusted(h) {
return h
}
}
return p
}
// Proto is "https" when the client spoke TLS: to this server, or to a
// trusted proxy that says so.
func Proto(r *http.Request) string {
if r.TLS != nil {
return "https"
}
if fromProxy(r) {
if v := first(r.Header.Get("X-Forwarded-Proto")); v != "" {
return strings.ToLower(v)
}
}
return "http"
}
// Host is the host the client asked for: Host, or what a trusted proxy says
// it was asked for.
func Host(r *http.Request) string {
if fromProxy(r) {
if v := first(r.Header.Get("X-Forwarded-Host")); v != "" {
return v
}
}
return r.Host
}
func first(v string) string {
if i := strings.IndexByte(v, ','); i >= 0 {
v = v[:i]
}
return strings.TrimSpace(v)
}