fix(proxy): break ties between equivalent login limit overrides deterministically

Two spellings of one network share a prefix length, so the exemption wins the tie, then the higher limit, regardless of mapping order

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-18 10:20:39 +00:00
parent 65765d6550
commit cfdf4fa5dc
4 changed files with 28 additions and 5 deletions

View file

@ -2759,7 +2759,7 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
)
max_failed_login_attempts_per_source_overrides: dict[str, int] | None = Field(
None,
description="Per-address overrides of `max_failed_login_attempts_per_source`, keyed by IP address or CIDR range, e.g. {'1.2.3.4': 200, '5.6.0.0/24': 500}. The most specific matching range wins, and the per-username allowance for that address follows as half the override. A value of 0 exempts the address from both limits. Set under `general_settings` in config.yaml",
description="Per-address overrides of `max_failed_login_attempts_per_source`, keyed by IP address or CIDR range, e.g. {'1.2.3.4': 200, '5.6.0.0/24': 500}. The most specific matching range wins (between equivalent keys such as '1.2.3.4' and '1.2.3.4/32', an exemption wins, then the higher limit), and the per-username allowance for that address follows as half the override. A value of 0 exempts the address from both limits. Set under `general_settings` in config.yaml",
)
failed_login_window_seconds: int | None = Field(
None,

View file

@ -186,10 +186,16 @@ def _parse_network(raw_range: str) -> _Network | None:
return None
def _precedence(network: _Network, limit: int) -> tuple[int, bool, int]:
"""Sort key for competing overrides: the longest prefix wins, then an exemption, then the higher limit."""
return (network.prefixlen, limit == EXEMPT, limit)
def _source_limit(settings: Mapping[str, object], client_ip: str) -> int:
"""Failure allowance for this address: the most specific configured range containing it, else the default.
``EXEMPT`` (0) means the operator opted this address out of both limits.
``EXEMPT`` (0) means the operator opted this address out of both limits. Between equivalent keys such as
``1.2.3.4`` and ``1.2.3.4/32`` an exemption wins, then the higher limit.
"""
default: Final = _int_setting(settings, SOURCE_LIMIT_KEY, DEFAULT_MAX_FAILED_LOGIN_ATTEMPTS_PER_SOURCE)
raw_overrides: Final = settings.get(SOURCE_LIMIT_OVERRIDES_KEY)
@ -206,11 +212,11 @@ def _source_limit(settings: Mapping[str, object], client_ip: str) -> int:
if address is None:
return default
matches: Final = sorted(
(network.prefixlen, _override_limit(raw_limit, default))
_precedence(network, _override_limit(raw_limit, default))
for raw_range, raw_limit in overrides.items()
if (network := _parse_network(raw_range)) is not None and address in network
)
return matches[-1][1] if matches else default
return matches[-1][-1] if matches else default
def user_limit_for(source_limit: int) -> int:

View file

@ -1023,6 +1023,23 @@ def test_source_overrides_pick_the_most_specific_matching_range():
assert _limit("::ffff:203.0.113.10") == 200
@pytest.mark.parametrize(
("overrides", "expected"),
[
({"203.0.113.7": 0, "203.0.113.7/32": 5}, None),
({"203.0.113.7/32": 5, "203.0.113.7": 0}, None),
({"203.0.113.0/24": 3, "203.0.113.9/24": 8}, 8),
({"203.0.113.9/24": 8, "203.0.113.0/24": 3}, 8),
],
ids=["exact-then-slash32", "slash32-then-exact", "low-then-high", "high-then-low"],
)
def test_equivalent_override_keys_resolve_to_the_exemption_then_the_higher_limit(overrides, expected):
"""Two spellings of the same network are a config mistake, so precedence must not depend on dict order."""
settings = {"trusted_proxy_ranges": ["10.0.0.0/8"], "max_failed_login_attempts_per_source_overrides": overrides}
assert _throttle_behind_trusted_proxy("203.0.113.7", settings).source_limit == expected
def test_ipv6_sources_are_grouped_by_their_64_bit_prefix():
"""A /64 holder has 2^64 addresses; counting each one separately would hand them unlimited fresh buckets."""
from litellm.proxy.auth.login_throttle import source_group

View file

@ -26571,7 +26571,7 @@ export interface components {
max_failed_login_attempts_per_source?: number | null;
/**
* Max Failed Login Attempts Per Source Overrides
* @description Per-address overrides of `max_failed_login_attempts_per_source`, keyed by IP address or CIDR range, e.g. {'1.2.3.4': 200, '5.6.0.0/24': 500}. The most specific matching range wins, and the per-username allowance for that address follows as half the override. A value of 0 exempts the address from both limits. Set under `general_settings` in config.yaml
* @description Per-address overrides of `max_failed_login_attempts_per_source`, keyed by IP address or CIDR range, e.g. {'1.2.3.4': 200, '5.6.0.0/24': 500}. The most specific matching range wins (between equivalent keys such as '1.2.3.4' and '1.2.3.4/32', an exemption wins, then the higher limit), and the per-username allowance for that address follows as half the override. A value of 0 exempts the address from both limits. Set under `general_settings` in config.yaml
*/
max_failed_login_attempts_per_source_overrides?: {
[key: string]: number;