package proxy_test import ( "encoding/json" "net/http" "testing" ) const ( // trustLocalhost trusts the address every test connects from, and a // network for proxies in front of it. trustLocalhost = localhost + "/32,10.0.0.0/8" // appHost is the host every test asks for. appHost = "app.example" // client is the client's address, as a proxy names it. client = "203.0.113.9" // forwardedFor is the header that lists the client and its proxies. forwardedFor = "X-Forwarded-For" // secure is the scheme a client reached traefik with. secure = "https" ) // appHeaders is what the app tells about the headers it received. type appHeaders struct { Host string `json:"host"` ForwardedFor string `json:"forwardedFor"` ForwardedHost string `json:"forwardedHost"` ForwardedProto string `json:"forwardedProto"` RealIP string `json:"realIp"` } // clientAddressCase is a request and what smallwebwaf makes of it. type clientAddressCase struct { name string env map[string]string header http.Header wantClient string wantApp appHeaders } func TestClientAddressAndForwardedHeaders(t *testing.T) { t.Parallel() for _, tc := range clientAddressCases() { t.Run(tc.name, func(t *testing.T) { t.Parallel() got, line := requestWithHeaders(t, tc.env, tc.header) tc.wantApp.Host = appHost if got != tc.wantApp { t.Errorf("app received %+v, want %+v", got, tc.wantApp) } if line.ClientIP != tc.wantClient || line.PeerIP != localhost { t.Errorf("log line has client_ip %q and peer_ip %q, want %q and %q", line.ClientIP, line.PeerIP, tc.wantClient, localhost) } }) } } // clientAddressCases are the requests TestClientAddressAndForwardedHeaders // sends, from 127.0.0.1, which the default trusted proxies leave out. func clientAddressCases() []clientAddressCase { trusted := map[string]string{trustedProxies: trustLocalhost} forged := http.Header{ forwardedFor: {client}, "X-Forwarded-Host": {"forged.example"}, "X-Forwarded-Proto": {secure}, "X-Real-Ip": {client}, } replaced := appHeaders{ ForwardedFor: localhost, ForwardedHost: appHost, ForwardedProto: "http", } return []clientAddressCase{{ name: "a peer outside the trusted proxies is the client, " + "and its forwarded headers are replaced", header: forged, wantClient: localhost, wantApp: replaced, }, { name: "set but empty, the trusted proxies trust nothing", env: map[string]string{trustedProxies: ""}, header: forged, wantClient: localhost, wantApp: replaced, }, { name: "behind a trusted peer, the client is the first address " + "outside the trusted proxies from the right", env: trusted, header: http.Header{ forwardedFor: {"198.51.100.7, " + client + ", 10.0.0.2"}, "X-Forwarded-Host": {appHost}, "X-Forwarded-Proto": {secure}, "X-Real-Ip": {client}, }, wantClient: client, wantApp: appHeaders{ ForwardedFor: "198.51.100.7, " + client + ", 10.0.0.2, " + localhost, ForwardedHost: appHost, ForwardedProto: secure, RealIP: client, }, }, { name: "when every address is a trusted proxy, the leftmost is the client", env: trusted, header: http.Header{forwardedFor: {"10.0.0.5, 10.0.0.2"}}, wantClient: "10.0.0.5", wantApp: appHeaders{ForwardedFor: "10.0.0.5, 10.0.0.2, " + localhost}, }, { name: "with no header, a trusted peer is the client", env: trusted, wantClient: localhost, wantApp: appHeaders{ForwardedFor: localhost}, }, { name: "an entry that is not an address ends the reading", env: trusted, header: http.Header{forwardedFor: {client + ", unknown, 10.0.0.2"}}, wantClient: "10.0.0.2", wantApp: appHeaders{ ForwardedFor: client + ", unknown, 10.0.0.2, " + localhost, }, }, { name: "several header lines are read as one list", env: trusted, header: http.Header{forwardedFor: {"2001:db8::7", "10.0.0.2"}}, wantClient: "2001:db8::7", wantApp: appHeaders{ForwardedFor: "2001:db8::7, 10.0.0.2, " + localhost}, }} } // requestWithHeaders sends a request for appHost with header through // smallwebwaf, with the settings in env, and returns the headers the app // received and the request's log line. func requestWithHeaders( t *testing.T, env map[string]string, header http.Header, ) (appHeaders, logLine) { t.Helper() app := startApp(t, func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(appHeaders{ Host: r.Host, ForwardedFor: r.Header.Get(forwardedFor), ForwardedHost: r.Header.Get("X-Forwarded-Host"), ForwardedProto: r.Header.Get("X-Forwarded-Proto"), RealIP: r.Header.Get("X-Real-Ip"), }) }) addr, out := startProxy(t, app.URL, env) req := newRequest(t, http.MethodGet, addr, "/", http.NoBody) req.Host = appHost req.Header = header.Clone() answered := do(t, req) var got appHeaders err := json.Unmarshal(answered.body, &got) if err != nil { t.Fatalf("decode the app's answer %q: %v", answered.body, err) } return got, out.requestLine(t) }