fix: resolve real visitor IP via CF-Connecting-IP for per-IP limiting
websocket.client.host is always the Cloudflare Tunnel machine's LAN IP for every internet-facing connection (the tunnel runs on a separate machine and terminates TLS there), which collapsed per-IP rate limiting into a single shared bucket for all remote visitors — the exact gap flagged in review. Cloudflare's edge sets CF-Connecting-IP itself, overwriting any client-supplied value, so it's safe to trust when present. Falls back to the raw socket peer for direct LAN/local access.
This commit is contained in:
@@ -232,3 +232,36 @@ async def test_summon_rate_limited_per_ip_even_with_fresh_account(
|
||||
assert rejection["message"] == (
|
||||
"The veil is crowded. The spirits need a moment before another summoning."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_summon_per_ip_bucket_follows_cf_connecting_ip_not_socket_peer(
|
||||
sync_client, monkeypatch
|
||||
):
|
||||
# Every connection in this test suite shares the same simulated socket
|
||||
# peer (TestClient has no real network). Without preferring
|
||||
# CF-Connecting-IP, distinct visitors behind the Cloudflare Tunnel would
|
||||
# collapse into one shared per-IP bucket — this proves they don't.
|
||||
monkeypatch.setattr(
|
||||
app.ws, "summon_ip_limiter", RateLimiter(max_requests=1, window_seconds=60)
|
||||
)
|
||||
|
||||
token_a = _login(sync_client, "cf-ip-a")
|
||||
with sync_client.websocket_connect(
|
||||
"/ws/session",
|
||||
headers={"cookie": f"qm_session={token_a}", "cf-connecting-ip": "203.0.113.1"},
|
||||
) as ws:
|
||||
_read_until(ws, "session")
|
||||
ws.send_json({"type": "summon"})
|
||||
_read_until(ws, "entity") # spends visitor A's per-IP slot only
|
||||
|
||||
token_b = _login(sync_client, "cf-ip-b")
|
||||
with sync_client.websocket_connect(
|
||||
"/ws/session",
|
||||
headers={"cookie": f"qm_session={token_b}", "cf-connecting-ip": "203.0.113.2"},
|
||||
) as ws:
|
||||
_read_until(ws, "session")
|
||||
ws.send_json({"type": "summon"})
|
||||
# A different visitor IP behind the same tunnel — must not be
|
||||
# rejected by visitor A's already-spent bucket.
|
||||
_read_until(ws, "entity")
|
||||
|
||||
Reference in New Issue
Block a user