Commit Graph
12 Commits
Author SHA1 Message Date
clawbot c26fffca1e Leave SWWAF_RATE_LIMIT_EXEMPT_PATHS out of the request rate limits (closes #77)
check / check (push) Waiting to run
A request is neither counted nor refused by the request rate limits
when its path as sent, the path the app receives, not percent-decoded,
starts with one of the comma-separated prefixes in
SWWAF_RATE_LIMIT_EXEMPT_PATHS, so /%61ssets/x is not under /assets/. A
request whose decoded path contains .. or a backslash, or whose path as
sent holds an encoded slash, is never exempt, since an app may act on
it as a path outside every prefix, such as /assets/..%2Flogin as
/login. The static lists, bans and the country lists still apply. The
setting is empty by default, and a prefix that does not start with /
stops the start. README.md documents it.

Model: opus-5-5
2026-10-06 13:46:00 +00:00
clawbot cff385af41 Observe mode: log what would be refused, refuse nothing (closes #78)
check / check (push) Successful in 3m54s
SWWAF_MODE (default enforce) takes enforce or observe. In observe mode a
request that SWWAF_DENY_NETS, a ban, the country lists or a rate limit
would refuse is passed to the app, and its log line names that refusal
in would_action. The size and time limits and the 401 still apply. A
broken limit makes no ban; bans read from bans.json are kept but refuse
nothing, and Ledger.Find reads them without counting a refusal in their
notes.

Judgement call: in observe mode a broken limit does not reset the
client's counters, since the reset comes with the ban.
Judgement call: a request a ban would refuse keeps ban_expires.

Model: opus-5-5
2026-10-06 13:04:43 +02:00
clawbot df2c5042d2 Keep the bans, the clients and GeoJS's answers in state files (closes #17)
check / check (push) Successful in 3m24s
smallwebwaf now copies its state to bans.json, clients.json and
lookups.json in SWWAF_STATE_DIR, as "Persistent state" in SPEC.md
describes, and reads them back at start, so a restart lifts no ban and
gives no client a fresh allowance. Each client gains a history, and a
ban's notes count the netblock's requests. bans.json is written
SWWAF_STATE_WRITE_DELAY after a ban, and every file every
SWWAF_STATE_COUNTER_INTERVAL and at the stop. A ban read back is masked
to its netblock and refuses every client in it. A file that does not
parse, an unknown version, an entry without a field it needs, or an
unwritable directory stops the start.

Deviation: no AS number or name, and no ban cause, reason or lifting yet.

Model: opus-5-5
2026-10-06 08:31:52 +02:00
clawbot 73ca94f850 Ban the netblock of a client that breaks a rate limit, in memory (closes #18)
check / check (push) Successful in 3m48s
A request over a rate limit is refused with SWWAF_BAN_RESPONSE and bans
the client's netblock: an hour at first, three times the last ban when
broken again within a day of its end, permanent past seven days. The
ban ledger in internal/bans is checked after the static lists and
before the lookup, and the requests it refuses are not counted. A ban
resets the client's counters and carries notes holding the request
that broke the limit, as SPEC.md now says. At most SWWAF_MAX_BANS are
held. SWWAF_BAN_RESPONSE also answers SWWAF_DENY_NETS and the country
lists.

Judgement call: the six ban settings cannot be off.
Judgement call: a permanent ban's ban_expires is "permanent".

Model: opus-5-5
2026-10-06 05:29:03 +02:00
clawbot 0f85c9ae07 The header size and the idle time as settings (closes #70)
check / check (push) Successful in 4m56s
SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES (default 32K) and
SWWAF_CLIENT_IDLE_TIMEOUT (default 120s) replace the two values the
proxy fixed. The idle time is read like the other durations, and can
be off.

Go's server reads 4K past the header limit it is given before it
refuses, so it is still given the setting less 4K. The header size
must be more than 4K and cannot be off; any other value stops the
start with a message that does not offer off.

SPEC.md and README.md say so. README.md lists both settings, no longer
calls them fixed, and names them as built.

Model: opus-5-5
2026-10-06 05:05:31 +02:00
clawbot 7f6f89cd83 Network lists: always allowed, exempt from rate limits, always refused (closes #19)
check / check (push) Successful in 3m47s
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
2026-10-06 02:36:27 +02:00
clawbot df4cf769e0 Derive the proxy tests' short timeout setting from the timeout (closes #61)
check / check (push) Successful in 4m14s
shortTimeoutSetting is now shortTimeout.String(), so changing waitLimit
changes both together.

Model: opus-5-5
2026-10-04 11:50:08 +02:00
clawbot e7fb88af9e Spend less of make test writing the image and waiting (closes #56)
check / check (push) Failing after 45s
The test phase spends most of its time compiling with the race
detector from an empty build cache; then come writing the test image
and the internal/proxy tests.

- Go's build cache is on a tmpfs in the test phase, so its 137 MB are
  no longer written into the test image.
- TestUpgradedConnectionOutlastsTheTimeouts waits until just past
  shortTimeout after the answer to the upgrade was read, by when every
  timeout has started, rather than 7.5 s, so it ends with the other
  timing tests.
- shortTimeout is written as waitLimit / 2, as its comment says it is.

Model: opus-5-5
2026-10-04 11:13:32 +02:00
clawbot 0750879e58 Country allow and deny lists, looked up through GeoJS (closes #44)
check / check (push) Failing after 3s
SWWAF_DENIED_COUNTRIES and SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES refuse a
request with 403 before its body is read or rate-limited, logged as
country_denied. internal/lookup asks GeoJS only while a list is set, 200
clients per request, one at a time, keeping answers 7 days. Failures, a
redirect or an answer leaving an address out included, are logged without
addresses; GeoJS is then left alone a second, doubling to five minutes.
Private, loopback and link-local clients are never sent.

Deviation, per the issue: no SWWAF_LOOKUP_SOURCE or SWWAF_LOOKUP_TIMEOUT; 403, not SWWAF_BAN_RESPONSE.
Deviation: GeoJS's country endpoint, not geo.json.
Judgement call: an IPv6 /64 is asked about by its first address; at most 10,000 clients wait.
Judgement call: config.go lists the ISO 3166-1 codes; no widely used library holds them.

Model: opus-5-5
2026-10-04 08:29:41 +02:00
clawbot 6977ff73df Proxy timing tests outlast a hold-up of the test process (closes #53)
check / check (push) Successful in 1m53s
smallwebwaf starts each timeout as the request arrives, before the step
a test needs first: the upgrade answered, the app's buffers full, the
first part of an answer passed on. A hold-up of the test process longer
than the 300 ms timeout ran it out before that step. No test can make
that step come first, and in the "waiting on the app" cases it cannot
see which side smallwebwaf was waiting on, so the timeout is now 5 s,
the hold-up wantTimedOut already allows. The timeout tests set when it
must not run out goes from 10 s to 1 m to stay clear of it. The upgrade
test waits 7.5 s past the upgrade.

Judgement call: one shared value; the proxy tests take about 8 s, not 2 s.

Model: opus-5-5
2026-10-04 07:53:18 +02:00
clawbot f51459fbfe Per-client request rate limits over a minute, an hour and a day (closes #43)
check / check (push) Successful in 2m50s
Each client, one IPv4 address or one IPv6 /64, is counted in two buckets
per window, the earlier weighted by how much of it the window covers; at
most 20,000 clients are kept, least recently seen dropped first. A
request over SWWAF_RATE_LIMIT_PER_MINUTE, _HOUR or _DAY (1000, 10000,
50000, or off) gets 429 before reaching the app. Refused requests count,
413s included. A clock set back over a second behind a bucket's start
restarts that window. The log line gains limit_hit and the action
rate_limited.

Deviation from SPEC.md, per the issue: the 20,000 bound and /64 are fixed.
Judgement call: golang-lru/v2 holds the table; httprate does not count refused requests.
Deviation: go.mod and go.sum hand-written; no make target tidies them.

Model: opus-5-5
2026-10-04 04:24:34 +02:00
clawbot d76715b0df Pass-through proxy with timeouts, size limits and a request log (closes #13)
check / check (push) Successful in 1m29s
Milestone 1, the repo's first code. smallwebwaf passes each request to the app and the answer back unchanged, streaming bodies and WebSocket upgrades, within four timeouts (client and app, request and response) and two size limits, and writes one JSON line per request to stdout. Every setting has an SWWAF_ name and a default, and an invalid value stops the start. The repo gets the standard layout: script/ entrypoints, make targets that call them, a Dockerfile that runs the checks, and the Gitea workflow.

Disclosure: SPEC.md changed. Go's server reads the request line and headers before smallwebwaf sees the request, so slow headers are closed without an answer, and neither slow nor oversized headers get a log line.
Disclosure: standard library only.

Model: opus-5-5
2026-10-03 17:24:34 +02:00