Pairing limits: no shared bucket for portal calls, a cap per side
Every call through the portal arrives off the tunnel, so the per-address limit skips those (the hub limits them by the real address), and the cap of 50 counts and evicts portal and local requests apart, so a portal flood never pushes out a device waiting on this network.
This commit is contained in:
@@ -97,12 +97,16 @@ CODE_LENGTH = 6
|
||||
#: The check a device shows beside its code, to compare with the pending list.
|
||||
CHECK_LENGTH = 2
|
||||
|
||||
#: Never more waiting than this: codes are free to ask for and cost memory.
|
||||
#: The oldest makes room for a new one, so a flood cannot lock a device out.
|
||||
#: Never more waiting than this from each way in, this network or the portal:
|
||||
#: codes are free to ask for and cost memory. The oldest of the same way in
|
||||
#: makes room for a new one, so a flood cannot lock a device out, and one
|
||||
#: through the portal never pushes out a device waiting here.
|
||||
MAX_PENDING = 50
|
||||
|
||||
#: Codes one address may ask for per window. A device asks once per code,
|
||||
#: every ten minutes, and again after a failure.
|
||||
#: Codes one address may ask for per window, here. A device asks once per
|
||||
#: code, every ten minutes, and again after a failure. Through the portal the
|
||||
#: hub limits each address and the instance by the address it sees, since
|
||||
#: here every such call arrives off the tunnel and would share one bucket.
|
||||
PAIR_PER_ADDRESS = 5
|
||||
PAIR_WINDOW_S = 60.0
|
||||
|
||||
@@ -197,10 +201,15 @@ class _PendingStore:
|
||||
self._redis.zremrangebyscore(_PENDING_INDEX, "-inf", now)
|
||||
return list(cast("list[str]", self._redis.zrange(_PENDING_INDEX, 0, -1)))
|
||||
|
||||
def make_room(self, room: int) -> None:
|
||||
"""Drop the oldest codes until fewer than ``room`` are left."""
|
||||
codes = self._codes()
|
||||
for code in codes[: max(0, len(codes) - room + 1)]:
|
||||
def make_room(self, room: int, remote: bool) -> None:
|
||||
"""Drop the oldest codes asked for the same way, through the portal
|
||||
or not, until fewer than ``room`` of them are left."""
|
||||
same = [
|
||||
code
|
||||
for code in self._codes()
|
||||
if (entry := self.get(code)) is not None and entry.remote == remote
|
||||
]
|
||||
for code in same[: max(0, len(same) - room + 1)]:
|
||||
self.drop(code)
|
||||
|
||||
def count(self) -> int:
|
||||
@@ -486,15 +495,15 @@ def start_pairing(request: Request, body: PairHello | None = None) -> Any:
|
||||
the cap and the portal's own limits are between. At the cap the oldest
|
||||
request makes room, so a flood delays a device rather than locking it out.
|
||||
"""
|
||||
if too_many(request, "pair", PAIR_PER_ADDRESS, PAIR_WINDOW_S):
|
||||
remote = cloud_config.via_portal(request.headers)
|
||||
if not remote and too_many(request, "pair", PAIR_PER_ADDRESS, PAIR_WINDOW_S):
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail="Too many pairing codes asked for from here. Try again shortly.",
|
||||
headers={"Retry-After": str(int(PAIR_WINDOW_S))},
|
||||
)
|
||||
_pending.make_room(MAX_PENDING)
|
||||
_pending.make_room(MAX_PENDING, remote)
|
||||
|
||||
remote = cloud_config.via_portal(request.headers)
|
||||
hello = body or PairHello()
|
||||
entry = _Pending(
|
||||
secret=secrets.token_urlsafe(16),
|
||||
|
||||
Reference in New Issue
Block a user