package rproxy import ( "net/http" "net/netip" "slices" "strings" ) // The headers rproxy sets on the requests it passes to the app. Go writes every // header name in this form, as it sends it and as it receives it, so // X-Client-ASN arrives as X-Client-Asn, and Del removes a visitor's own whatever // their case; header names are not case-sensitive. const ( requestIDHeader = "X-Request-ID" asnHeader = "X-Client-Asn" countryHeader = "X-Client-Country" ) // peerAddress is the address of the request's TCP peer, normally traefik, without // any IPv6 zone, which servewaf refuses. func peerAddress(r *http.Request) netip.Addr { addrPort, err := netip.ParseAddrPort(r.RemoteAddr) if err != nil { return netip.Addr{} } return addrPort.Addr().Unmap().WithZone("") } // clientAddress works out who the visitor is. A peer outside the trusted // proxies is the visitor, and what it says in X-Forwarded-For is ignored. For a // peer inside them, X-Forwarded-For is read from the right, and the first // address outside them is the visitor; if every address in it is inside, the // leftmost is, and with no header, the peer. An entry that is not an address // ends the reading, since nothing to its left can be believed. The visitor's // address never has an IPv6 zone, which servewaf refuses. func clientAddress( peer netip.Addr, forwardedFor []string, trusted []netip.Prefix, ) netip.Addr { client := peer if !isInside(peer, trusted) { return client } entries := strings.Split(strings.Join(forwardedFor, ","), ",") for _, entry := range slices.Backward(entries) { addr, err := netip.ParseAddr(strings.TrimSpace(entry)) if err != nil { break } client = addr.Unmap().WithZone("") if !isInside(client, trusted) { break } } return client } // scheme is how the visitor reached traefik, as a trusted proxy says in // X-Forwarded-Proto, or otherwise http, the only scheme rproxy serves. func scheme(r *http.Request, peerTrusted bool) string { proto := r.Header.Get("X-Forwarded-Proto") if !peerTrusted || proto == "" { return "http" } return proto } // isInside reports whether addr is in one of the netblocks. func isInside(addr netip.Addr, netblocks []netip.Prefix) bool { return slices.ContainsFunc(netblocks, func(netblock netip.Prefix) bool { return netblock.Contains(addr) }) } // setForwardedHeaders sets the headers in which the app learns about the // visitor, so that it sees what it would see from traefik directly. A trusted // proxy's forwarded headers pass on, with the proxy's own address added to // X-Forwarded-For. Those of any other peer are its own claims and are replaced: // X-Forwarded-For names the peer, X-Forwarded-Host the host it asked for, and // X-Forwarded-Proto plain http, which is how it reached rproxy. func setForwardedHeaders(in, out *http.Request, peer netip.Addr, trusted bool) { forwardedFor := peer.String() if trusted { // ReverseProxy removes these from out before Rewrite. for _, name := range []string{"Forwarded", "X-Forwarded-Host", "X-Forwarded-Proto"} { values, ok := in.Header[name] if ok { out.Header[name] = values } } prior := in.Header.Values("X-Forwarded-For") if len(prior) > 0 { forwardedFor = strings.Join(prior, ", ") + ", " + forwardedFor } out.Header.Set("X-Forwarded-For", forwardedFor) return } // ReverseProxy has removed Forwarded and the three set below; these are the // other headers in which traefik tells the app about the visitor and its // request. for _, name := range []string{ "X-Forwarded-Port", "X-Forwarded-Server", "X-Forwarded-Uri", "X-Forwarded-Method", "X-Forwarded-Prefix", "X-Forwarded-Tls-Client-Cert", "X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip", } { out.Header.Del(name) } out.Header.Set("X-Forwarded-For", forwardedFor) out.Header.Set("X-Forwarded-Host", in.Host) out.Header.Set("X-Forwarded-Proto", "http") }