Мониторинг и исправления защиты имени пользователя!

This commit is contained in:
gy9vin
2026-01-16 12:18:04 +03:00
parent 553e8e1ecd
commit ab492f3aef
4 changed files with 112 additions and 18 deletions

View File

@@ -88,6 +88,9 @@ SUSPICIOUS_NOTIFICATIONS_TOPIC_ID=14 # ID топика для увед
TRAFFIC_MONITORED_NODES= # Только эти ноды (пусто = все)
TRAFFIC_IGNORED_NODES= # Исключить эти ноды
# Исключить пользователей (UUID через запятую)
TRAFFIC_EXCLUDED_USER_UUIDS= # Служебные/тунельные пользователи
# Производительность
TRAFFIC_CHECK_BATCH_SIZE=1000 # Размер батча для получения пользователей
TRAFFIC_CHECK_CONCURRENCY=10 # Параллельных запросов к API

View File

@@ -256,6 +256,7 @@ class Settings(BaseSettings):
# Фильтрация по серверам (UUID нод через запятую)
TRAFFIC_MONITORED_NODES: str = "" # Только эти ноды (пусто = все)
TRAFFIC_IGNORED_NODES: str = "" # Исключить эти ноды
TRAFFIC_EXCLUDED_USER_UUIDS: str = "" # Исключить пользователей (UUID через запятую)
# Параллельность и кулдаун
TRAFFIC_CHECK_BATCH_SIZE: int = 1000 # Размер батча для получения пользователей
@@ -854,13 +855,31 @@ class Settings(BaseSettings):
"""Возвращает список UUID нод для мониторинга (пусто = все)"""
if not self.TRAFFIC_MONITORED_NODES:
return []
return [n.strip() for n in self.TRAFFIC_MONITORED_NODES.split(",") if n.strip()]
# Убираем комментарии (все после #)
value = self.TRAFFIC_MONITORED_NODES.split("#")[0].strip()
if not value:
return []
return [n.strip() for n in value.split(",") if n.strip()]
def get_traffic_ignored_nodes(self) -> List[str]:
"""Возвращает список UUID нод для исключения из мониторинга"""
if not self.TRAFFIC_IGNORED_NODES:
return []
return [n.strip() for n in self.TRAFFIC_IGNORED_NODES.split(",") if n.strip()]
# Убираем комментарии (все после #)
value = self.TRAFFIC_IGNORED_NODES.split("#")[0].strip()
if not value:
return []
return [n.strip() for n in value.split(",") if n.strip()]
def get_traffic_excluded_user_uuids(self) -> List[str]:
"""Возвращает список UUID пользователей для исключения из мониторинга (например, тунельные/служебные)"""
if not self.TRAFFIC_EXCLUDED_USER_UUIDS:
return []
# Убираем комментарии (все после #)
value = self.TRAFFIC_EXCLUDED_USER_UUIDS.split("#")[0].strip()
if not value:
return []
return [uuid.strip().lower() for uuid in value.split(",") if uuid.strip()]
def get_traffic_daily_check_time(self) -> Optional[time]:
"""Возвращает время суточной проверки трафика"""

View File

@@ -137,13 +137,28 @@ class DisplayNameRestrictionMiddleware(BaseMiddleware):
if any(pattern.search(lower_value) for pattern in LINK_PATTERNS):
return True
if DOMAIN_OBFUSCATION_PATTERN.search(lower_value):
return True
# Проверяем обфусцированные ссылки типа "t . m e" или "т м е"
# Но НЕ блокируем если это часть обычного слова/имени
domain_match = DOMAIN_OBFUSCATION_PATTERN.search(lower_value)
if domain_match:
# Проверяем контекст: если "tme" внутри слова (с буквами с обеих сторон) - пропускаем
start_pos = domain_match.start()
end_pos = domain_match.end()
# Проверяем символ ДО и ПОСЛЕ совпадения
has_letter_before = start_pos > 0 and lower_value[start_pos - 1].isalpha()
has_letter_after = end_pos < len(lower_value) and lower_value[end_pos].isalpha()
# Если с ОБЕИХ сторон буквы - скорее всего это просто имя/фамилия
if not (has_letter_before and has_letter_after):
return True
normalized = self._normalize_text(lower_value)
collapsed = COLLAPSE_PATTERN.sub("", normalized)
if "tme" in collapsed:
# Проверяем "tme" с контекстом (ловим t.me ссылки, но не случайные совпадения в именах)
# Ищем tme в начале, конце, или с пробелами/спецсимволами вокруг
if re.search(r"(?:^|[^a-zа-яё])tme(?:[^a-zа-яё]|$)", collapsed, re.IGNORECASE):
return True
banned_keywords = settings.get_display_name_banned_keywords()

View File

@@ -97,6 +97,9 @@ class TrafficMonitoringServiceV2:
def get_ignored_nodes(self) -> List[str]:
return settings.get_traffic_ignored_nodes()
def get_excluded_user_uuids(self) -> List[str]:
return settings.get_traffic_excluded_user_uuids()
def get_daily_check_time(self) -> Optional[time]:
return settings.get_traffic_daily_check_time()
@@ -133,7 +136,8 @@ class TrafficMonitoringServiceV2:
"""Загружает snapshot трафика из Redis"""
try:
snapshot_data = await cache.get(TRAFFIC_SNAPSHOT_KEY)
if snapshot_data and isinstance(snapshot_data, dict):
# ВАЖНО: пустой словарь {} - это валидный snapshot!
if snapshot_data is not None and isinstance(snapshot_data, dict):
# Конвертируем обратно в float
result = {uuid: float(bytes_val) for uuid, bytes_val in snapshot_data.items()}
logger.debug(f"📦 Snapshot загружен из Redis: {len(result)} пользователей")
@@ -176,6 +180,23 @@ class TrafficMonitoringServiceV2:
logger.error(f"❌ Ошибка получения времени уведомления: {e}")
return None
# ============== Работа с нодами ==============
async def _load_nodes_cache(self):
"""Загружает названия нод в кеш"""
try:
nodes = await self.remnawave_service.get_all_nodes()
self._nodes_cache = {node['uuid']: node['name'] for node in nodes if node.get('uuid') and node.get('name')}
logger.debug(f"📋 Загружено {len(self._nodes_cache)} нод в кеш")
except Exception as e:
logger.error(f"❌ Ошибка загрузки нод в кеш: {e}")
def get_node_name(self, node_uuid: Optional[str]) -> Optional[str]:
"""Возвращает название ноды по UUID из кеша"""
if not node_uuid:
return None
return self._nodes_cache.get(node_uuid)
# ============== Фильтрация по нодам ==============
def should_monitor_node(self, node_uuid: Optional[str]) -> bool:
@@ -274,13 +295,13 @@ class TrafficMonitoringServiceV2:
async def has_snapshot(self) -> bool:
"""Проверяет, есть ли сохранённый snapshot (Redis + fallback на память)"""
# Проверяем Redis
# Проверяем Redis (пустой словарь {} - это тоже валидный snapshot!)
snapshot = await self._load_snapshot_from_redis()
if snapshot:
if snapshot is not None:
return True
# Fallback на память
return bool(self._memory_snapshot) and self._memory_snapshot_time is not None
return self._memory_snapshot_time is not None
async def get_snapshot_age_minutes(self) -> float:
"""Возвращает возраст snapshot в минутах (Redis + fallback на память)"""
@@ -328,9 +349,9 @@ class TrafficMonitoringServiceV2:
Если в Redis уже есть snapshot — использует его (персистентность).
Возвращает количество пользователей в snapshot.
"""
# Проверяем есть ли snapshot в Redis
# Проверяем есть ли snapshot в Redis (пустой {} тоже валидный snapshot!)
existing_snapshot = await self._load_snapshot_from_redis()
if existing_snapshot:
if existing_snapshot is not None:
age = await self.get_snapshot_age_minutes()
logger.info(
f"📦 Найден существующий snapshot в Redis: {len(existing_snapshot)} пользователей, "
@@ -382,6 +403,24 @@ class TrafficMonitoringServiceV2:
start_time = datetime.utcnow()
is_first_run = not await self.has_snapshot()
# Загружаем кеш нод для красивых названий в уведомлениях
await self._load_nodes_cache()
# Логируем фильтры
monitored_nodes = self.get_monitored_nodes()
ignored_nodes = self.get_ignored_nodes()
excluded_user_uuids = self.get_excluded_user_uuids()
if monitored_nodes:
logger.info(f"🔍 Мониторим только ноды: {monitored_nodes}")
elif ignored_nodes:
logger.info(f"🚫 Игнорируем ноды: {ignored_nodes}")
else:
logger.info(f"📊 Мониторим все ноды")
if excluded_user_uuids:
logger.info(f"🚫 Исключены пользователи: {excluded_user_uuids}")
if is_first_run:
logger.info("🚀 Первый запуск быстрой проверки — создаём snapshot...")
else:
@@ -396,7 +435,7 @@ class TrafficMonitoringServiceV2:
# Загружаем предыдущий snapshot (из Redis или памяти)
previous_snapshot = await self._get_current_snapshot()
logger.debug(f"📦 Предыдущий snapshot: {len(previous_snapshot)} пользователей")
logger.info(f"📦 Предыдущий snapshot: {len(previous_snapshot)} пользователей (is_first_run={is_first_run})")
checked_users = 0
users_with_delta = 0
@@ -420,6 +459,7 @@ class TrafficMonitoringServiceV2:
# Пользователя не было в предыдущем snapshot — пропускаем (новый пользователь)
if user.uuid not in previous_snapshot:
logger.debug(f"Пользователь {user.uuid[:8]} не найден в предыдущем snapshot, пропускаем")
continue
# Получаем предыдущее значение
@@ -437,15 +477,22 @@ class TrafficMonitoringServiceV2:
if delta_bytes < threshold_bytes:
continue
logger.info(f"⚠️ Превышение дельты: {user.uuid[:8]}... +{delta_gb:.2f} ГБ (порог {self.get_fast_check_threshold_gb()} ГБ)")
logger.info(f"⚠️ Превышение дельты: {user.uuid[:8]}... +{delta_gb:.2f} ГБ (порог {self.get_fast_check_threshold_gb()} ГБ, previous={previous_bytes / (1024**3):.2f} ГБ, current={current_bytes / (1024**3):.2f} ГБ)")
# Проверяем исключённых пользователей (служебные/тунельные)
if user.uuid.lower() in excluded_user_uuids:
logger.info(f"⏭️ Пропускаем {user.uuid[:8]}... - пользователь в списке исключений (служебный/тунельный)")
continue
# Проверяем фильтр по нодам
last_node_uuid = user_traffic.last_connected_node_uuid
if not self.should_monitor_node(last_node_uuid):
logger.warning(f"⏭️ Пропускаем {user.uuid[:8]} - нода {last_node_uuid or 'неизвестна'} не в списке мониторинга")
continue
# Создаём violation
delta_gb = round(delta_bytes / (1024 ** 3), 2)
node_name = self.get_node_name(last_node_uuid)
violation = TrafficViolation(
user_uuid=user.uuid,
telegram_id=user.telegram_id,
@@ -454,7 +501,7 @@ class TrafficMonitoringServiceV2:
used_traffic_gb=delta_gb, # Это дельта, не общий трафик!
threshold_gb=self.get_fast_check_threshold_gb(),
last_node_uuid=last_node_uuid,
last_node_name=None,
last_node_name=node_name,
check_type="fast"
)
violations.append(violation)
@@ -464,6 +511,7 @@ class TrafficMonitoringServiceV2:
# Обновляем snapshot (в Redis с fallback на память)
await self._save_snapshot(new_snapshot)
logger.info(f"💾 Новый snapshot сохранён: {len(new_snapshot)} пользователей")
elapsed = (datetime.utcnow() - start_time).total_seconds()
@@ -495,6 +543,9 @@ class TrafficMonitoringServiceV2:
logger.info("🚀 Запуск суточной проверки трафика...")
start_time = datetime.utcnow()
# Загружаем кеш нод для красивых названий в уведомлениях
await self._load_nodes_cache()
violations: List[TrafficViolation] = []
threshold_bytes = self.get_daily_threshold_gb() * (1024 ** 3)
@@ -537,6 +588,7 @@ class TrafficMonitoringServiceV2:
return None
used_gb = round(total_bytes / (1024 ** 3), 2)
node_name = self.get_node_name(last_node_uuid)
return TrafficViolation(
user_uuid=user.uuid,
telegram_id=user.telegram_id,
@@ -545,7 +597,7 @@ class TrafficMonitoringServiceV2:
used_traffic_gb=used_gb,
threshold_gb=self.get_daily_threshold_gb(),
last_node_uuid=last_node_uuid,
last_node_name=None,
last_node_name=node_name,
check_type="daily"
)
@@ -594,7 +646,7 @@ class TrafficMonitoringServiceV2:
for i, violation in enumerate(violations):
try:
if not await self.should_send_notification(violation.user_uuid):
logger.debug(f"⏭️ Кулдаун для {violation.user_uuid}, пропускаем")
logger.info(f"⏭️ Кулдаун для {violation.user_uuid[:8]}... - пропускаем уведомление (кулдаун {self.get_notification_cooldown_seconds() // 60} мин)")
continue
# Получаем информацию о пользователе из БД
@@ -632,8 +684,13 @@ class TrafficMonitoringServiceV2:
f"🚨 Превышение: <b>{violation.used_traffic_gb - violation.threshold_gb:.2f} ГБ</b>\n"
)
if violation.last_node_uuid:
message += f"\n🖥 Последняя нода: <code>{violation.last_node_uuid}</code>"
# Показываем название ноды и UUID
if violation.last_node_name:
message += f"\n🖥 Сервер: <b>{violation.last_node_name}</b>"
if violation.last_node_uuid:
message += f"\n <code>{violation.last_node_uuid}</code>"
elif violation.last_node_uuid:
message += f"\n🖥 Сервер: <code>{violation.last_node_uuid}</code>"
message += f"\n\n{datetime.utcnow().strftime('%d.%m.%Y %H:%M:%S')} UTC"