check / check (push) Waiting to run
Each request log line now has the fields "Request log" in SPEC.md lists whose features are built: instance (SWWAF_INSTANCE_NAME), scheme, request_id (a trusted proxy's X-Request-ID or a new one, sent on to the app), forwarded_for, client_group, content_type, content_length, the headers SWWAF_LOG_REQUEST_HEADERS names, has_authorization, has_cookie, websocket, response_content_type, cache_control, location, counts and the timings. Authorization, Cookie and Set-Cookie values are never logged. An entry of SWWAF_LOG_REQUEST_HEADERS that is not a header name stops the start. Deviation: counts has request totals only. Deviation: SWWAF_INSTANCE_NAME is on request lines only. Model: opus-5-5
145 lines
4.3 KiB
Go
145 lines
4.3 KiB
Go
package proxy
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"net/http"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// peerAddress is the address of the request's TCP peer, normally traefik.
|
|
func peerAddress(r *http.Request) netip.Addr {
|
|
addrPort, err := netip.ParseAddrPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return netip.Addr{}
|
|
}
|
|
|
|
return addrPort.Addr().Unmap()
|
|
}
|
|
|
|
// clientAddress works out who the client is. A peer outside the trusted
|
|
// proxies is the client, 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 client; 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.
|
|
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()
|
|
if !isInside(client, trusted) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
// requestIDHeader carries the request's id, from traefik and to the app.
|
|
const requestIDHeader = "X-Request-ID"
|
|
|
|
// requestID is the request's id: the one a trusted proxy sent, or a new
|
|
// random one. A peer outside the trusted proxies did not come through
|
|
// traefik, so the id it sends is its own claim, and is replaced.
|
|
func requestID(r *http.Request, peerTrusted bool) string {
|
|
id := r.Header.Get(requestIDHeader)
|
|
if !peerTrusted || id == "" {
|
|
id = rand.Text()
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
// scheme is how the client reached traefik, as a trusted proxy says in
|
|
// X-Forwarded-Proto, or otherwise http, the only scheme smallwebwaf
|
|
// serves.
|
|
func scheme(r *http.Request, peerTrusted bool) string {
|
|
proto := r.Header.Get("X-Forwarded-Proto")
|
|
if !peerTrusted || proto == "" {
|
|
return "http"
|
|
}
|
|
|
|
return proto
|
|
}
|
|
|
|
// ipv6GroupPrefix is the length of the IPv6 netblock that is one client.
|
|
const ipv6GroupPrefix = 64
|
|
|
|
// clientGroup is the client a request is counted toward: its IPv4
|
|
// address, or the /64 its IPv6 address is in, since one abuser usually
|
|
// holds a whole /64. An IPv4 address in IPv6 form counts as IPv4.
|
|
func clientGroup(addr netip.Addr) netip.Prefix {
|
|
addr = addr.Unmap()
|
|
if addr.Is6() {
|
|
return netip.PrefixFrom(addr, ipv6GroupPrefix).Masked()
|
|
}
|
|
|
|
return netip.PrefixFrom(addr, addr.BitLen())
|
|
}
|
|
|
|
// 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
|
|
// client, 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
|
|
// smallwebwaf.
|
|
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
|
|
// client 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")
|
|
}
|