package proxy_test import ( "net/http" "strings" "sync/atomic" "testing" "sneak.berlin/go/smallwebwaf/internal/requestlog" ) // The rate limits count an IPv6 client by its /64, so these two addresses // are one client for them. The static lists match each address on its own, // and the tests list listedAddr alone. const ( listedAddr = "2001:db8::1" unlistedAddr = "2001:db8::2" ) func TestAllowNetsSkipEveryCheckButTheSizeLimit(t *testing.T) { t.Parallel() var calls atomic.Int32 app := startApp(t, func(http.ResponseWriter, *http.Request) { calls.Add(1) }) geojsURL, asked := startGeoJS(t) // fromKP is in SWWAF_ALLOW_NETS, and in SWWAF_DENY_NETS too, which // comes after it. addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{ trustedProxies: trustLocalhost, allowNets: "198.51.100.0/24", denyNets: fromKP, deniedCountries: "kp", rateLimitPerMinute: "1", requestMaxBytes: "1K", }) // Neither SWWAF_DENY_NETS, the country lists nor the limit of one // request a minute refuses the client, and its country is not looked // up. wantAnswers(t, addr, out, []sentRequest{ {fromKP, http.StatusOK, requestlog.ActionForward}, {fromKP, http.StatusOK, requestlog.ActionForward}, }) if len(asked()) != 0 { t.Errorf("GeoJS was asked about %v, want nothing", asked()) } // The size limit still applies. body := strings.NewReader(strings.Repeat("a", 2<<10)) req := newRequest(t, http.MethodPost, addr, "/", body) req.Header.Set(forwardedFor, fromKP) wantStatus(t, do(t, req), http.StatusRequestEntityTooLarge) wantLine(t, out.requestLines(t, 3)[2], http.StatusRequestEntityTooLarge, requestlog.ActionTooLarge) if calls.Load() != 2 { t.Errorf("the app was called %d times, want 2", calls.Load()) } } func TestRequestFromAllowNetsIsNotCounted(t *testing.T) { t.Parallel() app := startApp(t, func(http.ResponseWriter, *http.Request) {}) addr, out := startProxy(t, app.URL, map[string]string{ trustedProxies: trustLocalhost, allowNets: listedAddr, rateLimitPerMinute: "1", }) // listedAddr's requests are not counted, so the first request from // unlistedAddr is within the limit of one a minute. wantAnswers(t, addr, out, []sentRequest{ {listedAddr, http.StatusOK, requestlog.ActionForward}, {listedAddr, http.StatusOK, requestlog.ActionForward}, {unlistedAddr, http.StatusOK, requestlog.ActionForward}, {unlistedAddr, http.StatusForbidden, requestlog.ActionRateLimited}, }) } func TestDenyNetsRefuseBeforeTheLookupAndTheBody(t *testing.T) { t.Parallel() var calls atomic.Int32 app := startApp(t, func(http.ResponseWriter, *http.Request) { calls.Add(1) }) geojsURL, asked := startGeoJS(t) addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{ trustedProxies: trustLocalhost, denyNets: "203.0.113.0/24", deniedCountries: "kp", }) req := newRequest(t, http.MethodPost, addr, "/", strings.NewReader("a body")) req.Header.Set(forwardedFor, fromDE) wantStatus(t, do(t, req), http.StatusForbidden) line := out.requestLine(t) wantLine(t, line, http.StatusForbidden, requestlog.ActionDenied) if line.RequestBytes != 0 { t.Errorf("log line has request_bytes %d, want 0", line.RequestBytes) } if len(asked()) != 0 { t.Errorf("GeoJS was asked about %v, want nothing", asked()) } if calls.Load() != 0 { t.Errorf("the app was called %d times, want none", calls.Load()) } } func TestRequestRefusedByDenyNetsIsNotCounted(t *testing.T) { t.Parallel() app := startApp(t, func(http.ResponseWriter, *http.Request) {}) addr, out := startProxy(t, app.URL, map[string]string{ trustedProxies: trustLocalhost, denyNets: listedAddr, rateLimitPerMinute: "1", }) // listedAddr's refused requests are not counted, so the first request // from unlistedAddr is within the limit of one a minute. wantAnswers(t, addr, out, []sentRequest{ {listedAddr, http.StatusForbidden, requestlog.ActionDenied}, {listedAddr, http.StatusForbidden, requestlog.ActionDenied}, {unlistedAddr, http.StatusOK, requestlog.ActionForward}, {unlistedAddr, http.StatusForbidden, requestlog.ActionRateLimited}, }) } func TestRateLimitExemptNetsAreNeitherCountedNorRefused(t *testing.T) { t.Parallel() app := startApp(t, func(http.ResponseWriter, *http.Request) {}) geojsURL, _ := startGeoJS(t) addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{ trustedProxies: trustLocalhost, rateLimitExemptNets: listedAddr + "," + fromKP, deniedCountries: "kp", rateLimitPerMinute: "1", }) // listedAddr's requests are neither refused nor counted, so the first // request from unlistedAddr is within the limit of one a minute. The // country lists still refuse an exempt client. wantAnswers(t, addr, out, []sentRequest{ {listedAddr, http.StatusOK, requestlog.ActionForward}, {listedAddr, http.StatusOK, requestlog.ActionForward}, {unlistedAddr, http.StatusOK, requestlog.ActionForward}, {unlistedAddr, http.StatusForbidden, requestlog.ActionRateLimited}, {fromKP, http.StatusForbidden, requestlog.ActionCountryDenied}, }) } // sentRequest is a GET request from client, as X-Forwarded-For names it, // and the status and log line action it should get. type sentRequest struct { client string status int action string } // wantAnswers sends requests to smallwebwaf at addr one after another and // checks each one's answer and log line. They must be the first requests // smallwebwaf is sent, since the log lines are matched to them in order. func wantAnswers(t *testing.T, addr string, out *output, requests []sentRequest) { t.Helper() for i, sent := range requests { req := newRequest(t, http.MethodGet, addr, "/", http.NoBody) req.Header.Set(forwardedFor, sent.client) wantStatus(t, do(t, req), sent.status) wantLine(t, out.requestLines(t, i+1)[i], sent.status, sent.action) } }