diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/middleware.py | 10 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_rate_limit_key.py | 48 |
2 files changed, 55 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/middleware.py b/packages/meshbay-hub/src/meshbay_hub/api/middleware.py index bed7b54..3a2a5c3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/middleware.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/middleware.py @@ -7,7 +7,11 @@ to mitigate credential stuffing and registration floods. """ from slowapi import Limiter -from slowapi.util import get_remote_address -# Rate limiter instance — mounted on the FastAPI app in app.py -limiter = Limiter(key_func=get_remote_address) +from meshbay_hub.api.netutil import client_ip + +# Rate limiter instance — mounted on the FastAPI app in app.py. +# Keyed on client_ip, not slowapi's get_remote_address: behind Caddy every +# request's peer is loopback, so the peer address put the whole internet in one +# bucket — ten node sign-ins a minute, shared by every node there is. +limiter = Limiter(key_func=client_ip) diff --git a/packages/meshbay-hub/tests/test_rate_limit_key.py b/packages/meshbay-hub/tests/test_rate_limit_key.py new file mode 100644 index 0000000..679f546 --- /dev/null +++ b/packages/meshbay-hub/tests/test_rate_limit_key.py @@ -0,0 +1,48 @@ +""" +Rate limits are per client, not per proxy. + +meshbay.org's hub sits behind Caddy on the same host, so every request's TCP peer +is loopback. The limiter was keyed on that peer (slowapi's get_remote_address) +while `client_ip` -- written "for the audit log and rate limiting" -- went +unused by it, so each limit was one bucket for the whole internet: ten node +sign-ins a minute shared by every node. A node that started while others signed +in got 429, sat in `waiting_for_hub`, and the desktop app took that for no node +at all. Every other test runs with the limiter disabled, which is how it hid. +""" + +import pytest +from meshbay_hub.api.middleware import limiter +from meshbay_hub.api.netutil import client_ip + + +@pytest.fixture +def limits_on(): + limiter.reset() + limiter.enabled = True + yield + limiter.enabled = False + limiter.reset() + + +def _auth(client, ip): + # The ASGI test transport's peer is 127.0.0.1 -- a trusted proxy, as Caddy is. + return client.post("/v1/nodes/auth", + json={"username": "nobody", "timestamp": 0, "signature": "AAAA"}, + headers={"X-Forwarded-For": ip}) + + +async def test_one_client_is_limited(client, limits_on): + for _ in range(10): + assert (await _auth(client, "203.0.113.1")).status_code != 429 + assert (await _auth(client, "203.0.113.1")).status_code == 429 + + +async def test_another_client_behind_the_same_proxy_is_not(client, limits_on): + for _ in range(11): + await _auth(client, "203.0.113.1") + assert (await _auth(client, "203.0.113.2")).status_code != 429, ( + "a second client behind the proxy shared the first one's bucket") + + +def test_the_limiter_resolves_clients_the_way_the_audit_log_does(): + assert limiter._key_func is client_ip |