redis cache/ webhook abuse/ separate streams for sending and requesting panels/ increased security and more

This commit is contained in:
Vladless
2026-02-26 22:00:15 +03:00
parent 405c3fef1b
commit 6f76608b04
43 changed files with 945 additions and 269 deletions
Binary file not shown.
+16 -3
View File
@@ -1,7 +1,7 @@
UPDATE_STALE_AGE_SEC = 60
CONCURRENCY_MAX_WAIT_SEC = 300
CONCURRENCY_LIMIT = 25
CONCURRENCY_LIMIT = 200
SUBSCRIPTION_CACHE_SUBSCRIBED_MAXSIZE = 200_000
SUBSCRIPTION_CACHE_SUBSCRIBED_TTL_SEC = 300
@@ -25,7 +25,7 @@ USER_EXISTS_CACHE_MAXSIZE = 150_000
USER_EXISTS_CACHE_TTL_SEC = 60
BAN_CACHE_MAXSIZE = 50_000
BAN_CACHE_TTL_SEC = 30
BAN_CACHE_TTL_SEC = 60
DIRECT_START_USER_EXISTS_CACHE_MAXSIZE = 50_000
DIRECT_START_USER_EXISTS_CACHE_TTL_SEC = 20
@@ -37,7 +37,8 @@ REMNAWAVE_SERVER_CACHE_MAXSIZE = 50_000
REMNAWAVE_SERVER_CACHE_TTL_SEC = 300
REMNAWAVE_PROFILE_CACHE_MAXSIZE = 200_000
REMNAWAVE_PROFILE_CACHE_TTL_SEC = 20
REMNAWAVE_PROFILE_TIMEOUT_SEC = 3.0
REMNAWAVE_PROFILE_TIMEOUT_SEC = 10.0
REMNAWAVE_PROFILE_ERROR_CACHE_TTL_SEC = 45
REMNAWAVE_ACTION_TIMEOUT_SEC = 5.0
REMNAWAVE_MAX_CONCURRENCY = 20
@@ -51,7 +52,19 @@ SERVERS_CACHE_TTL_SEC = 60
TARIFF_BY_ID_CACHE_TTL_SEC = 120
TARIFFS_FOR_CLUSTER_CACHE_TTL_SEC = 120
KEY_DETAILS_CACHE_TTL_SEC = 45
KEYS_LIST_CACHE_TTL_SEC = 25
KEY_COUNT_CACHE_TTL_SEC = 25
BALANCE_CACHE_TTL_SEC = 25
PROFILE_DATA_CACHE_TTL_SEC = 25
PAYMENT_PENDING_CACHE_TTL_SEC = 3600
ERROR_THROTTLE_WINDOW_SEC = 60
ERROR_THROTTLE_MAX_KEYS = 500
ERROR_THROTTLE_MESSAGE_MAX_LEN = 120
WEBHOOK_ABUSE_FAIL_THRESHOLD = 10
WEBHOOK_ABUSE_FAIL_WINDOW_SEC = 60
WEBHOOK_ABUSE_BLOCK_TTL_SEC = 300
+14
View File
@@ -77,6 +77,20 @@ def shutdown_process_pool() -> None:
logger.debug("[Executor] Пул процессов остановлен")
def should_run_heavy_tasks_separately() -> bool:
"""
True, если есть запас по ядрам/потокам — тогда рассылка и уведомления
можно выносить в отдельный поток/ядро.
"""
try:
from config import EXECUTOR_POOL_SIZE
pool_size = max(1, int(EXECUTOR_POOL_SIZE))
except Exception:
pool_size = 1
cpu_count = multiprocessing.cpu_count() or 1
return cpu_count >= 2 or pool_size >= 2
async def run_io(fn: Callable[..., T], *args: object) -> T:
"""Выполняет fn(*args) в пуле потоков (I/O). Один вызов для всех блокирующих операций."""
loop = asyncio.get_running_loop()
+52
View File
@@ -0,0 +1,52 @@
from aiohttp import web
from core.cache_config import (
WEBHOOK_ABUSE_BLOCK_TTL_SEC,
WEBHOOK_ABUSE_FAIL_THRESHOLD,
WEBHOOK_ABUSE_FAIL_WINDOW_SEC,
)
from core.redis_cache import cache_delete, cache_get, cache_incr, cache_key, cache_set
def get_webhook_client_ip(request: web.Request) -> str:
"""IP клиента: X-Forwarded-For (первый) или X-Real-IP, иначе request.remote."""
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip() or "unknown"
real = request.headers.get("X-Real-IP")
if real:
return real.strip() or "unknown"
if request.remote:
s = str(request.remote)
if "%" in s:
s = s.split("%")[0]
if ":" in s:
s = s.rsplit(":", 1)[0]
return s or "unknown"
return "unknown"
async def is_webhook_ip_blocked(ip: str) -> bool:
"""True, если IP временно заблокирован из‑за множества невалидных подписей."""
if not ip or ip == "unknown":
return False
try:
block_key = cache_key("webhook_abuse_block", ip)
return (await cache_get(block_key)) is not None
except Exception:
return False
async def record_webhook_signature_failure(ip: str) -> None:
"""Увеличивает счётчик неудачных проверок подписи для IP; при превышении порога блокирует IP."""
if not ip or ip == "unknown":
return
try:
fail_key = cache_key("webhook_abuse_fail", ip)
count = await cache_incr(fail_key, WEBHOOK_ABUSE_FAIL_WINDOW_SEC)
if count >= WEBHOOK_ABUSE_FAIL_THRESHOLD:
block_key = cache_key("webhook_abuse_block", ip)
await cache_set(block_key, 1, WEBHOOK_ABUSE_BLOCK_TTL_SEC)
await cache_delete(fail_key)
except Exception:
pass