feat: add SOCKS5 proxy support for Telegram API traffic

Route bot traffic through SOCKS5 proxy when PROXY_URL env var is set.
Validates scheme to reject HTTP proxies (would expose bot token).
Credentials are masked in logs.
This commit is contained in:
Fringg
2026-03-21 02:32:35 +03:00
parent 2a72deadd6
commit 82b6a8bf70
6 changed files with 70 additions and 2 deletions
+5
View File
@@ -13,6 +13,11 @@ SUPPORT_USERNAME=@support
# Имя пользователя бота (опционально, автоопределяется) # Имя пользователя бота (опционально, автоопределяется)
# BOT_USERNAME= # BOT_USERNAME=
# ===== SOCKS5 ПРОКСИ =====
# URL SOCKS5 прокси-сервера для маршрутизации трафика бота к Telegram API
# Формат: socks5://user:password@host:port или socks5://host:port
# PROXY_URL=socks5://127.0.0.1:1080
# ===== СИСТЕМА ПОДДЕРЖКИ ===== # ===== СИСТЕМА ПОДДЕРЖКИ =====
# Включить меню поддержки в интерфейсе # Включить меню поддержки в интерфейсе
SUPPORT_MENU_ENABLED=true SUPPORT_MENU_ENABLED=true
+13 -1
View File
@@ -99,7 +99,19 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
bot = Bot(token=settings.BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) proxy_url = settings.get_proxy_url()
session = None
if proxy_url:
from urllib.parse import urlparse
from aiogram.client.session.aiohttp import AiohttpSession
session = AiohttpSession(proxy=proxy_url)
parsed = urlparse(proxy_url)
masked = f'{parsed.scheme}://***@{parsed.hostname}:{parsed.port}' if parsed.username else proxy_url
logger.info('Proxy configured', proxy_url=masked)
bot = Bot(token=settings.BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML), session=session)
maintenance_service.set_bot(bot) maintenance_service.set_bot(bot)
logger.info('Бот установлен в maintenance_service') logger.info('Бот установлен в maintenance_service')
+25
View File
@@ -802,6 +802,27 @@ class Settings(BaseSettings):
BAN_SYSTEM_API_TOKEN: str | None = None BAN_SYSTEM_API_TOKEN: str | None = None
BAN_SYSTEM_REQUEST_TIMEOUT: int = 30 BAN_SYSTEM_REQUEST_TIMEOUT: int = 30
# SOCKS5 proxy for routing bot traffic to Telegram API
# Format: socks5://user:password@host:port or socks5://host:port
PROXY_URL: str | None = None
@field_validator('PROXY_URL', mode='before')
@classmethod
def validate_proxy_url(cls, value: str | None) -> str | None:
if not value:
return None
from urllib.parse import urlparse
parsed = urlparse(value)
if parsed.scheme not in ('socks5', 'socks4'):
raise ValueError(
f'PROXY_URL must use socks5:// or socks4:// scheme, got: {parsed.scheme!r}. '
'HTTP proxies are not supported for security reasons (bot token would be exposed).'
)
if not parsed.hostname:
raise ValueError('PROXY_URL must contain a hostname')
return value
@field_validator('MAIN_MENU_MODE', mode='before') @field_validator('MAIN_MENU_MODE', mode='before')
@classmethod @classmethod
def normalize_main_menu_mode(cls, value: str | None) -> str: def normalize_main_menu_mode(cls, value: str | None) -> str:
@@ -928,6 +949,10 @@ class Settings(BaseSettings):
"""Проверяет, используется ли SQLite""" """Проверяет, используется ли SQLite"""
return 'sqlite' in self.get_database_url() return 'sqlite' in self.get_database_url()
def get_proxy_url(self) -> str | None:
"""Return SOCKS5 proxy URL or None."""
return self.PROXY_URL if self.PROXY_URL else None
def is_admin(self, telegram_id: int | None = None, email: str | None = None) -> bool: def is_admin(self, telegram_id: int | None = None, email: str | None = None) -> bool:
""" """
Check if user is admin by telegram_id or email. Check if user is admin by telegram_id or email.
+1
View File
@@ -24,6 +24,7 @@ dependencies = [
'pyzipper>=0.3.6', 'pyzipper>=0.3.6',
'structlog>=25.1.0,<26', 'structlog>=25.1.0,<26',
'rich>=14.0', 'rich>=14.0',
'aiohttp-socks>=0.10.1',
] ]
[dependency-groups] [dependency-groups]
+1
View File
@@ -1,6 +1,7 @@
# Основные зависимости # Основные зависимости
aiogram==3.22.0 aiogram==3.22.0
aiohttp==3.12.15 aiohttp==3.12.15
aiohttp-socks>=0.10.1
asyncpg==0.31.0 asyncpg==0.31.0
SQLAlchemy==2.0.46 SQLAlchemy==2.0.46
alembic==1.18.4 alembic==1.18.4
Generated
+25 -1
View File
@@ -71,6 +71,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" },
] ]
[[package]]
name = "aiohttp-socks"
version = "0.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "python-socks" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/cc/e5bbd54f76bd56291522251e47267b645dac76327b2657ade9545e30522c/aiohttp_socks-0.11.0.tar.gz", hash = "sha256:0afe51638527c79077e4bd6e57052c87c4824233d6e20bb061c53766421b10f0", size = 11196, upload-time = "2025-12-09T13:35:52.564Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/7d/4b633d709b8901d59444d2e512b93e72fe62d2b492a040097c3f7ba017bb/aiohttp_socks-0.11.0-py3-none-any.whl", hash = "sha256:9aacce57c931b8fbf8f6d333cf3cafe4c35b971b35430309e167a35a8aab9ec1", size = 10556, upload-time = "2025-12-09T13:35:50.18Z" },
]
[[package]] [[package]]
name = "aiosignal" name = "aiosignal"
version = "1.4.0" version = "1.4.0"
@@ -1057,6 +1070,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" },
] ]
[[package]]
name = "python-socks"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/36/0b/cd77011c1bc01b76404f7aba07fca18aca02a19c7626e329b40201217624/python_socks-2.8.1.tar.gz", hash = "sha256:698daa9616d46dddaffe65b87db222f2902177a2d2b2c0b9a9361df607ab3687", size = 38909, upload-time = "2026-02-16T05:24:00.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/fe/9a58cb6eec633ff6afae150ca53c16f8cc8b65862ccb3d088051efdfceb7/python_socks-2.8.1-py3-none-any.whl", hash = "sha256:28232739c4988064e725cdbcd15be194743dd23f1c910f784163365b9d7be035", size = 55087, upload-time = "2026-02-16T05:23:59.147Z" },
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.3" version = "6.0.3"
@@ -1115,10 +1137,11 @@ wheels = [
[[package]] [[package]]
name = "remnawave-bedolaga-telegram-bot" name = "remnawave-bedolaga-telegram-bot"
version = "3.34.1" version = "3.36.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiogram" }, { name = "aiogram" },
{ name = "aiohttp-socks" },
{ name = "aiosqlite" }, { name = "aiosqlite" },
{ name = "alembic" }, { name = "alembic" },
{ name = "asyncpg" }, { name = "asyncpg" },
@@ -1149,6 +1172,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "aiogram", specifier = ">=3.25.0" }, { name = "aiogram", specifier = ">=3.25.0" },
{ name = "aiohttp-socks", specifier = ">=0.10.1" },
{ name = "aiosqlite", specifier = ">=0.22.1" }, { name = "aiosqlite", specifier = ">=0.22.1" },
{ name = "alembic", specifier = ">=1.18.4" }, { name = "alembic", specifier = ">=1.18.4" },
{ name = "asyncpg", specifier = ">=0.31.0" }, { name = "asyncpg", specifier = ">=0.31.0" },