Network lists: always allowed, exempt from rate limits, always refused (closes #19)
check / check (push) Failing after 2s
check / check (push) Failing after 2s
Adds SWWAF_ALLOW_NETS, SWWAF_RATE_LIMIT_EXEMPT_NETS and SWWAF_DENY_NETS, read like SWWAF_TRUSTED_PROXIES and empty by default, and checked against the client's own address before its country is looked up. A client in SWWAF_ALLOW_NETS skips the country lists and the rate limits and is not looked up. One in SWWAF_DENY_NETS is refused with 403, logged as denied and not counted. One in SWWAF_RATE_LIMIT_EXEMPT_NETS is neither counted nor refused by the rate limits. SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES now refuses a private, loopback or link-local client unless SWWAF_ALLOW_NETS lists it. Judgement call: an address in both SWWAF_ALLOW_NETS and SWWAF_DENY_NETS is let through. Judgement call: the size and time limits still apply to SWWAF_ALLOW_NETS. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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 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.StatusTooManyRequests, 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.StatusTooManyRequests, 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 in turn, as the first
|
||||
// it is sent, and checks each one's answer and log line.
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user