v0.26.1 per-client connection caps

This commit is contained in:
lixen committed 2026-10-05 02:22:30 -04:00
1 parent ee01cb00fe
commit cdd4fc01a7
36 files changed
+795 -417

No files matched your search

+48 -5
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env bash
# logwisp acl test: each listener kind admits or refuses peers by address
# before TLS, or by the client a PROXY header names (stub proxy, nginx if
# installed), counts and warns of refusals, and throttles logins per client.
# before TLS, or by the client a PROXY header or a proxy's X-Forwarded-For
# names (stub proxy, nginx if installed), counts and warns of refusals, and
# caps connections, requests and logins per client.
# One-shot. Ports: 15881-15889 on 127.0.0.1, the proxies' on 127.0.0.11-14.
# Requires: bash 5+, curl, nc, go; Linux.
@@ -102,6 +103,10 @@ check "a deny list alone on a wildcard listener warns" "$(warns 'tcp,port=15881,
check "an allow entry of a whole family warns" "$(warns 'tcp,port=15881,acl.allow=0.0.0.0/0' 'admits every address of its family')"
check "a proxy_from entry reaching public addresses warns" \
"$(warns 'tcp,host=127.0.0.1,port=15881,acl.proxy_protocol=optional,acl.proxy_from=203.0.113.0/24' 'proxy_from reaches public addresses')"
check "a request rate on a tcp listener is refused" \
"$(rejects 'tcp,host=127.0.0.1,port=15881,acl.requests_per_second_per_client=5' 'applies only to HTTP listeners')"
check "a connection cap behind trusted_proxies, which would count the proxy, is refused" \
"$(rejects "http,host=127.0.0.1,port=15881,auth.type=scram,auth.credentials_file=$RUN/users.toml,auth.trusted_proxies=127.0.0.1,acl.max_connections_per_client=2" 'behind trusted_proxies')"
check "optional proxy_protocol warns that a headerless client passes as the proxy" \
"$(warns 'tcp,host=127.0.0.1,port=15881,acl.proxy_protocol=optional,acl.proxy_from=127.0.0.1' 'passes as the proxy')"
@@ -134,6 +139,25 @@ check "a client the acl admits gets all 20000 lines of a finite input ($n)" "$((
check "one it denies gets nothing ($denied bytes) and is logged" \
"$( ((denied == 0)) && grep -q 'refused by acl.* remote_addr 127.0.0.2:' "$LOG/tcp.err" && echo 1 || echo 0)"
section "Behind an L7 proxy"
# proxy mode over plaintext loopback: curl is the proxy, its headers name the client
spawn web-proxied "$BIN" --source null --sink "http,host=127.0.0.1,port=15882,auth.type=scram,auth.credentials_file=$RUN/users.toml,auth.trusted_proxies=127.0.0.1,acl.deny=198.51.100.5,acl.deny=2001:db8::/32,acl.requests_per_second_per_client=1"
wait_port 15882 || abort "the proxy-mode http sink did not listen" web-proxied
# as CLIENT [PATH [CURL ARGS]]: the status of a request forwarded for CLIENT,
# GET /status by default; there 401 is past the acl, short of a login
as() { get -o /dev/null -w '%{http_code}' -H "X-Forwarded-For: $1" -H 'X-Forwarded-Proto: https' "${@:3}" "http://127.0.0.1:15882${2:-/status}"; }
codes="$(as 198.51.100.5) $(as 2001:db8::5)"
wait_until 5 logged web-proxied 'Request refused by acl.* remote_addr 198.51.100.5 .*address rules'
check "forwarded clients the acl denies, of either family, are refused per request, and logged once ($codes)" \
"$( [[ $codes == '403 403' && $(count web-proxied 'refused by acl') == 1 ]] && echo 1 || echo 0)"
codes="$(as 198.51.100.7) $(as 198.51.100.7)"
check "a forwarded client past requests_per_second_per_client gets 429 ($codes)" "$(is "$codes" '401 429')"
code=$(as 198.51.100.8)
check "while another behind the same proxy is not ($code)" "$(is "$code" 401)"
hello=(-H 'Content-Type: application/json' -d '{"logwisp":1,"scram":{"username":"edge-01","client_nonce":"nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn"}}')
codes="$(as 198.51.100.9 /auth "${hello[@]}") $(as 198.51.100.9 /auth "${hello[@]}")"
check "the SCRAM exchange, which its throttling limits, is not counted in the rate ($codes)" "$(is "$codes" '200 200')"
section "Chain sources"
# pin DAEMON: the pin_sha256 a self-signed listener logged
pin() { wait_until 5 logged "$1" pin_sha256 && grep -o 'pin_sha256 "sha256//[^"]*"' "$LOG/$1.out" | head -1 | cut -d'"' -f2; }
@@ -158,6 +182,7 @@ section "Behind a PROXY header"
behind=acl.proxy_protocol=required,acl.proxy_from=127.0.0.1
scram="tls.enabled=true,tls.self_signed=true,auth.type=scram,auth.credentials_file=$RUN/users.toml"
spawn sink-behind "$BIN" --logging.level=info --source null --sink "http,host=127.0.0.1,port=15886,$scram,$behind,acl.deny=198.51.100.2"
spawn tcp-capped "$BIN" --source null --sink "tcp,host=127.0.0.1,port=15889,$behind,acl.max_connections_per_client=2"
for t in tcp http; do
spawn "agg-$t-behind" "$BIN" --logging.level=info \
--source "${t}_chain,host=127.0.0.1,port=$([[ $t == tcp ]] && echo 15888 || echo 15883),$scram,$behind" \
@@ -167,11 +192,12 @@ done
{ wait_until 30 test -e "$RUN/feed"; seq 1 100 | sed 's/^/behind /'; } |
lw --sink "tcp,host=127.0.0.1,port=15887,$behind,acl.deny=198.51.100.2,acl.deny=127.0.0.6" 2>"$LOG/tcp-behind.out" &
fpid=$!
for p in 15883 15886 15887 15888; do wait_port $p || abort "a listener behind the proxy did not listen on $p" sink-behind; done
for p in 15883 15886 15887 15888 15889; do wait_port $p || abort "a listener behind the proxy did not listen on $p" sink-behind; done
spawn proxy "$RUN/proxy/proxy" 127.0.0.11:15886,127.0.0.1:15886,1,198.51.100.1 \
127.0.0.12:15886,127.0.0.1:15886,2,198.51.100.2 127.0.0.13:15886,127.0.0.1:15886,2,198.51.100.3 \
127.0.0.11:15887,127.0.0.1:15887,2,198.51.100.1 127.0.0.12:15887,127.0.0.1:15887,1,198.51.100.2 \
127.0.0.11:15888,127.0.0.1:15888,1,198.51.100.1 127.0.0.13:15883,127.0.0.1:15883,2,198.51.100.3
127.0.0.11:15888,127.0.0.1:15888,1,198.51.100.1 127.0.0.13:15883,127.0.0.1:15883,2,198.51.100.3 \
127.0.0.11:15889,127.0.0.1:15889,1,198.51.100.1 127.0.0.12:15889,127.0.0.1:15889,2,198.51.100.2
wait_until 10 logged proxy ready || abort "the stub proxy did not start" proxy
NGINX=0
if command -v nginx >/dev/null; then
@@ -214,8 +240,9 @@ touch "$RUN/feed"
wait "$fpid" "${pids[@]}"
lines() { grep -c '^behind ' "$RUN/$1.out"; }
check "a tcp sink streams to the client a v2 header names ($(lines admitted) lines)" "$(($(lines admitted) == 100))"
# the WARN names whichever denied client came first, the stub's or nginx's
check "and refuses one a v1 header names, logging both addresses ($(lines denied) lines)" \
"$( (($(lines denied) == 0)) && grep -q 'refused by acl.* remote_addr 198.51.100.2:.*peer_addr 127.0.0.1:' "$LOG/tcp-behind.out" && echo 1 || echo 0)"
"$( (($(lines denied) == 0)) && grep -Eq 'refused by acl.* remote_addr (198.51.100.2|127.0.0.6):.*peer_addr 127.0.0.1:' "$LOG/tcp-behind.out" && echo 1 || echo 0)"
if ((NGINX)); then
check "nginx stream with proxy_protocol on names its clients ($(lines nginx) lines, $(lines nginx-denied) to a denied one)" \
"$( (($(lines nginx) == 100 && $(lines nginx-denied) == 0)) && echo 1 || echo 0)"
@@ -223,6 +250,22 @@ else
skip "nginx stream with proxy_protocol on (nginx with its stream module is not available)"
fi
# tcp sink capped at two connections per client: two held, then a third and another client's
held() { timeout 2 nc "$@" 15889 </dev/null >/dev/null; (($? == 124)) && echo 1 || echo 0; } # NC_ARGS... ADDR
pids=()
for from in 127.0.0.11 127.0.0.11 '-s 127.0.0.2 127.0.0.1' '-s 127.0.0.2 127.0.0.1'; do
# shellcheck disable=SC2086 # NC_ARGS
timeout 6 nc $from 15889 </dev/null >/dev/null &
pids+=($!)
done
sleep 1
third=$(held 127.0.0.11) other=$(held 127.0.0.12)
check "a third connection from the client a header names is closed, another client's held ($third, $other)" \
"$( ((!third && other)) && grep -q 'refused by acl.* remote_addr 198.51.100.1:.*max_connections_per_client.*peer_addr 127.0.0.1:' "$LOG/tcp-capped.out" && echo 1 || echo 0)"
third=$(held -s 127.0.0.2 127.0.0.1) other=$(held -s 127.0.0.3 127.0.0.1)
check "and so for direct peers, outside proxy_from ($third, $other)" "$( ((!third && other)) && echo 1 || echo 0)"
wait "${pids[@]}"
# http sink: each client its own login budget behind the one proxy address
PIN=$(pin sink-behind)
token() { lw auth token --url "https://$1:15886" -u viewer-01 --password-file "$2" --pin-sha256 "$PIN" 2>&1; }