mirror of https://github.com/zulip/zulip.git
rate_limiter: Block IPv6 by /64.
Fixes #21544. rate limits IPv6 by /64 prefx rather than individual addresses, this makes all IPs of the same /64 belong to the same bucket.
This commit is contained in:
parent
00c9f36434
commit
5d7615879f
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from ipaddress import IPv6Address
|
||||
from typing import Optional, cast
|
||||
|
||||
import orjson
|
||||
|
@ -138,8 +139,9 @@ class RateLimitedUser(RateLimitedObject):
|
|||
|
||||
|
||||
class RateLimitedIPAddr(RateLimitedObject):
|
||||
def __init__(self, ip_addr: str, domain: str = "api_by_ip") -> None:
|
||||
def __init__(self, ip_addr: str, domain: str = "api_by_ip", ipv6_prefix: int = 64) -> None:
|
||||
self.ip_addr = ip_addr
|
||||
self.ipv6_prefix = ipv6_prefix
|
||||
self.domain = domain
|
||||
if settings.RUNNING_INSIDE_TORNADO and domain in settings.RATE_LIMITING_DOMAINS_FOR_TORNADO:
|
||||
backend: type[RateLimiterBackend] | None = TornadoInMemoryRateLimiterBackend
|
||||
|
@ -149,8 +151,15 @@ class RateLimitedIPAddr(RateLimitedObject):
|
|||
|
||||
@override
|
||||
def key(self) -> str:
|
||||
# ipv6, we use the prefix as the key.
|
||||
if ":" in self.ip_addr:
|
||||
ip_addr_key = get_ipv6_prefix_in_hex(self.ip_addr, prefix=self.ipv6_prefix)
|
||||
|
||||
else:
|
||||
ip_addr_key = self.ip_addr
|
||||
|
||||
# The angle brackets are important since IPv6 addresses contain :.
|
||||
return f"{type(self).__name__}:<{self.ip_addr}>:{self.domain}"
|
||||
return f"{type(self).__name__}:<{ip_addr_key}>:{self.domain}"
|
||||
|
||||
@override
|
||||
def rules(self) -> list[tuple[int, int]]:
|
||||
|
@ -603,6 +612,23 @@ def rate_limit_request_by_ip(request: HttpRequest, domain: str) -> None:
|
|||
RateLimitedIPAddr(ip_addr, domain=domain).rate_limit_request(request)
|
||||
|
||||
|
||||
def get_ipv6_prefix_in_hex(ipv6: str, prefix: int) -> str:
|
||||
"""Return the hex representation of the /prefix block of ipv6,
|
||||
which acts as the bucket key for this ipv6.
|
||||
|
||||
examples:
|
||||
|
||||
2001:0db8:ce1:12::8a2e:0370 /64 -> 2001:0db8:ce1:12
|
||||
|
||||
2001:0db8:0::8a2e:0370 /64 -> 2001:0db8:0000:0000
|
||||
|
||||
2001:0db8:0::8a2e:0370 /48 -> 2001:0db8:0000
|
||||
"""
|
||||
# Since prefix represents the number of bits:
|
||||
# 1 hex char = 4 bits, then No.hex chars = No.bits / 4
|
||||
return IPv6Address(ipv6).__format__("x")[: prefix // 4]
|
||||
|
||||
|
||||
def should_rate_limit(request: HttpRequest) -> bool:
|
||||
if not settings.RATE_LIMITING:
|
||||
return False
|
||||
|
|
|
@ -200,18 +200,26 @@ class TornadoInMemoryRateLimiterBackendTest(RateLimiterBackendBase):
|
|||
|
||||
def test_used_in_tornado(self) -> None:
|
||||
user_profile = self.example_user("hamlet")
|
||||
ip_addr = "192.168.0.123"
|
||||
ipv4_addr = "192.168.0.123"
|
||||
ipv6_addr = "2002:DB8::21f:5bff:febf:ce22:1111"
|
||||
|
||||
with self.settings(RUNNING_INSIDE_TORNADO=True):
|
||||
user_obj = RateLimitedUser(user_profile, domain="api_by_user")
|
||||
ip_obj = RateLimitedIPAddr(ip_addr, domain="api_by_ip")
|
||||
ipv4_obj = RateLimitedIPAddr(ipv4_addr, domain="api_by_ip")
|
||||
ipv6_obj = RateLimitedIPAddr(ipv6_addr, domain="api_by_ip")
|
||||
|
||||
self.assertEqual(user_obj.backend, TornadoInMemoryRateLimiterBackend)
|
||||
self.assertEqual(ip_obj.backend, TornadoInMemoryRateLimiterBackend)
|
||||
self.assertEqual(ipv4_obj.backend, TornadoInMemoryRateLimiterBackend)
|
||||
self.assertEqual(ipv6_obj.backend, TornadoInMemoryRateLimiterBackend)
|
||||
|
||||
with self.settings(RUNNING_INSIDE_TORNADO=True):
|
||||
user_obj = RateLimitedUser(user_profile, domain="some_domain")
|
||||
ip_obj = RateLimitedIPAddr(ip_addr, domain="some_domain")
|
||||
ipv4_obj = RateLimitedIPAddr(ipv4_addr, domain="some_domain")
|
||||
ipv6_obj = RateLimitedIPAddr(ipv6_addr, domain="some_domain")
|
||||
|
||||
self.assertEqual(user_obj.backend, RedisRateLimiterBackend)
|
||||
self.assertEqual(ip_obj.backend, RedisRateLimiterBackend)
|
||||
self.assertEqual(ipv4_obj.backend, RedisRateLimiterBackend)
|
||||
self.assertEqual(ipv6_obj.backend, RedisRateLimiterBackend)
|
||||
|
||||
def test_block_access(self) -> None:
|
||||
obj = self.create_object("test", [(2, 5)])
|
||||
|
@ -249,6 +257,19 @@ class RateLimitedObjectsTest(ZulipTestCase):
|
|||
obj = RateLimitedTestObject("test", rules=[], backend=RedisRateLimiterBackend)
|
||||
self.assertEqual(obj.get_rules(), [(1, 9999)])
|
||||
|
||||
def test_ip_bucket_key(self) -> None:
|
||||
ipv6_addr = "2001:db8:4a2b:21fe::4"
|
||||
ipv4_addr = "192.168.0.123"
|
||||
domain = "api_by_ip"
|
||||
|
||||
ipv4_bucket_key = RateLimitedIPAddr(ipv4_addr, domain=domain).key()
|
||||
ipv6_64_bucket_key = RateLimitedIPAddr(ipv6_addr, domain=domain, ipv6_prefix=64).key()
|
||||
ipv6_48_bucket_key = RateLimitedIPAddr(ipv6_addr, domain=domain, ipv6_prefix=48).key()
|
||||
|
||||
self.assertEqual(ipv4_bucket_key, f"RateLimitedIPAddr:<{ipv4_addr}>:{domain}")
|
||||
self.assertEqual(ipv6_64_bucket_key, f"RateLimitedIPAddr:<20010db84a2b21fe>:{domain}")
|
||||
self.assertEqual(ipv6_48_bucket_key, f"RateLimitedIPAddr:<20010db84a2b>:{domain}")
|
||||
|
||||
|
||||
# Don't load the base class as a test: https://bugs.python.org/issue17519.
|
||||
del RateLimiterBackendBase
|
||||
|
|
|
@ -259,8 +259,11 @@ DEFAULT_RATE_LIMITING_RULES = {
|
|||
],
|
||||
# Limits total number of unauthenticated API requests (primarily
|
||||
# used by the public access option). Since these are
|
||||
# unauthenticated requests, each IP address is a separate bucket.
|
||||
# unauthenticated requests, each IPv4 address is a separate bucket.
|
||||
# For IPv6, one bucket is used for each IPv6/64.
|
||||
"api_by_ip": [
|
||||
# be IPv4 or IPv6/64.
|
||||
# 100 requests per minute.
|
||||
(60, 100),
|
||||
],
|
||||
# Limits total requests to the Mobile Push Notifications Service
|
||||
|
@ -308,6 +311,7 @@ DEFAULT_RATE_LIMITING_RULES = {
|
|||
# sending of an email, restricting the number per IP address. This
|
||||
# is a general anti-spam measure.
|
||||
"sends_email_by_ip": [
|
||||
# 5 emails per day.
|
||||
(86400, 5),
|
||||
],
|
||||
# Limits access to uploaded files, in web-public contexts, done by
|
||||
|
|
Loading…
Reference in New Issue