py-mtproxy-lib 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mtproxy/__init__.py +23 -0
- mtproxy/cli.py +192 -0
- mtproxy/config.py +161 -0
- mtproxy/daemon.py +192 -0
- mtproxy/logger.py +102 -0
- mtproxy/server.py +235 -0
- mtproxy/stats.py +167 -0
- py_mtproxy_lib-0.0.1.dist-info/METADATA +52 -0
- py_mtproxy_lib-0.0.1.dist-info/RECORD +12 -0
- py_mtproxy_lib-0.0.1.dist-info/WHEEL +5 -0
- py_mtproxy_lib-0.0.1.dist-info/entry_points.txt +2 -0
- py_mtproxy_lib-0.0.1.dist-info/top_level.txt +1 -0
mtproxy/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
py-mtproxy-lib - Python библиотека для запуска MTProto прокси
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .server import MTProxyServer
|
|
6
|
+
from .utils import generate_secret
|
|
7
|
+
from .stats import ProxyStats, ConnectionStats
|
|
8
|
+
from .logger import setup_logger
|
|
9
|
+
from .config import ProxyConfig
|
|
10
|
+
from .daemon import Daemon
|
|
11
|
+
from .cli import main
|
|
12
|
+
|
|
13
|
+
__version__ = "2.0.0"
|
|
14
|
+
__all__ = [
|
|
15
|
+
'MTProxyServer',
|
|
16
|
+
'generate_secret',
|
|
17
|
+
'ProxyStats',
|
|
18
|
+
'ConnectionStats',
|
|
19
|
+
'setup_logger',
|
|
20
|
+
'ProxyConfig',
|
|
21
|
+
'Daemon',
|
|
22
|
+
'main'
|
|
23
|
+
]
|
mtproxy/cli.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""CLI интерфейс"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
from .config import ProxyConfig
|
|
7
|
+
from .server import MTProxyServer
|
|
8
|
+
from .utils import generate_secret
|
|
9
|
+
from .daemon import Daemon
|
|
10
|
+
from .logger import setup_logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def start_server(config: ProxyConfig, foreground: bool = False):
|
|
14
|
+
"""Запуск сервера"""
|
|
15
|
+
# Настройка логгера
|
|
16
|
+
logger = setup_logger(
|
|
17
|
+
level=config.log_level,
|
|
18
|
+
log_file=config.log_file
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
server = MTProxyServer(config)
|
|
22
|
+
|
|
23
|
+
if foreground:
|
|
24
|
+
server.start()
|
|
25
|
+
else:
|
|
26
|
+
daemon = Daemon(
|
|
27
|
+
pidfile=config.pid_file or '/var/run/mtproxy.pid',
|
|
28
|
+
stdout=config.log_file or '/dev/null',
|
|
29
|
+
stderr=config.log_file or '/dev/null'
|
|
30
|
+
)
|
|
31
|
+
daemon.start(server.start)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main():
|
|
35
|
+
"""Основная функция CLI"""
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
description="MTProto Proxy Server",
|
|
38
|
+
prog="mtproxy"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"-c", "--config",
|
|
43
|
+
help="Config file path",
|
|
44
|
+
type=str,
|
|
45
|
+
default="mtproxy.conf"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"-s", "--secret",
|
|
50
|
+
help="Add secret (can be used multiple times)",
|
|
51
|
+
action="append"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"-p", "--port",
|
|
56
|
+
help="Port to listen on",
|
|
57
|
+
type=int
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--no-fake-tls",
|
|
62
|
+
help="Disable Fake TLS masking",
|
|
63
|
+
action="store_true"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"-d", "--daemon",
|
|
68
|
+
help="Run as daemon",
|
|
69
|
+
action="store_true"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--pid-file",
|
|
74
|
+
help="PID file path",
|
|
75
|
+
type=str
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--log-file",
|
|
80
|
+
help="Log file path",
|
|
81
|
+
type=str
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--log-level",
|
|
86
|
+
help="Log level",
|
|
87
|
+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
|
88
|
+
default='INFO'
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--stats-interval",
|
|
93
|
+
help="Statistics output interval (seconds)",
|
|
94
|
+
type=int
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--generate-secret",
|
|
99
|
+
help="Generate random secret and exit",
|
|
100
|
+
action="store_true"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--show-links",
|
|
105
|
+
help="Show connection links",
|
|
106
|
+
action="store_true"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Команды управления демоном
|
|
110
|
+
subparsers = parser.add_subparsers(dest="command", help="Daemon commands")
|
|
111
|
+
|
|
112
|
+
subparsers.add_parser("start", help="Start daemon")
|
|
113
|
+
subparsers.add_parser("stop", help="Stop daemon")
|
|
114
|
+
subparsers.add_parser("restart", help="Restart daemon")
|
|
115
|
+
subparsers.add_parser("status", help="Check daemon status")
|
|
116
|
+
|
|
117
|
+
args = parser.parse_args()
|
|
118
|
+
|
|
119
|
+
# Генерация секрета
|
|
120
|
+
if args.generate_secret:
|
|
121
|
+
print(generate_secret())
|
|
122
|
+
sys.exit(0)
|
|
123
|
+
|
|
124
|
+
# Загрузка конфигурации
|
|
125
|
+
config = None
|
|
126
|
+
if os.path.exists(args.config):
|
|
127
|
+
try:
|
|
128
|
+
config = ProxyConfig.from_file(args.config)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
print(f"Error loading config: {e}", file=sys.stderr)
|
|
131
|
+
|
|
132
|
+
if not config:
|
|
133
|
+
config = ProxyConfig()
|
|
134
|
+
|
|
135
|
+
# Применение CLI аргументов
|
|
136
|
+
if args.port:
|
|
137
|
+
config.port = args.port
|
|
138
|
+
if args.no_fake_tls:
|
|
139
|
+
config.enable_fake_tls = False
|
|
140
|
+
if args.pid_file:
|
|
141
|
+
config.pid_file = args.pid_file
|
|
142
|
+
if args.log_file:
|
|
143
|
+
config.log_file = args.log_file
|
|
144
|
+
if args.log_level:
|
|
145
|
+
config.log_level = args.log_level
|
|
146
|
+
if args.stats_interval:
|
|
147
|
+
config.stats_interval = args.stats_interval
|
|
148
|
+
|
|
149
|
+
# Добавление секретов
|
|
150
|
+
if args.secret:
|
|
151
|
+
for secret in args.secret:
|
|
152
|
+
config.add_secret(secret)
|
|
153
|
+
|
|
154
|
+
# Если нет секретов, создаем один
|
|
155
|
+
if not config.secrets:
|
|
156
|
+
config.add_secret()
|
|
157
|
+
print(f"Generated default secret: {list(config.secrets.keys())[0]}")
|
|
158
|
+
|
|
159
|
+
# Показ ссылок
|
|
160
|
+
if args.show_links:
|
|
161
|
+
links = config.get_connection_links()
|
|
162
|
+
print("Connection links:")
|
|
163
|
+
for name, link in links.items():
|
|
164
|
+
print(f" {name}: {link}")
|
|
165
|
+
sys.exit(0)
|
|
166
|
+
|
|
167
|
+
# Команды демона
|
|
168
|
+
daemon = Daemon(
|
|
169
|
+
pidfile=config.pid_file or '/var/run/mtproxy.pid',
|
|
170
|
+
stdout=config.log_file or '/dev/null',
|
|
171
|
+
stderr=config.log_file or '/dev/null'
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if args.command == "start":
|
|
175
|
+
sys.exit(daemon.start(start_server, config))
|
|
176
|
+
elif args.command == "stop":
|
|
177
|
+
sys.exit(daemon.stop())
|
|
178
|
+
elif args.command == "restart":
|
|
179
|
+
sys.exit(daemon.restart(start_server, config))
|
|
180
|
+
elif args.command == "status":
|
|
181
|
+
print(daemon.status())
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
|
|
184
|
+
# Запуск в foreground или daemon
|
|
185
|
+
if args.daemon:
|
|
186
|
+
daemon.start(start_server, config)
|
|
187
|
+
else:
|
|
188
|
+
start_server(config, foreground=True)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
main()
|
mtproxy/config.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Конфигурация с поддержкой нескольких секретов"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Dict, List, Optional, Tuple
|
|
7
|
+
from .utils import generate_secret, verify_fake_tls_handshake
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ProxyConfig:
|
|
12
|
+
"""
|
|
13
|
+
Конфигурация прокси сервера с поддержкой нескольких секретов
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
secrets (Dict[str, str]): Словарь {secret: tag}
|
|
17
|
+
port (int): Порт для прослушивания
|
|
18
|
+
enable_fake_tls (bool): Включить Fake TLS маскировку
|
|
19
|
+
handshake_timeout (int): Таймаут handshake в секундах
|
|
20
|
+
stats_interval (int): Интервал вывода статистики (сек)
|
|
21
|
+
log_level (str): Уровень логирования
|
|
22
|
+
log_file (Optional[str]): Файл лога
|
|
23
|
+
pid_file (Optional[str]): Файл PID
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
secrets: Dict[str, str] = field(default_factory=dict)
|
|
27
|
+
port: int = 443
|
|
28
|
+
enable_fake_tls: bool = True
|
|
29
|
+
handshake_timeout: int = 10
|
|
30
|
+
stats_interval: int = 60
|
|
31
|
+
log_level: str = "INFO"
|
|
32
|
+
log_file: Optional[str] = None
|
|
33
|
+
pid_file: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_dict(cls, data: dict) -> 'ProxyConfig':
|
|
37
|
+
"""Создание конфигурации из словаря"""
|
|
38
|
+
secrets = {}
|
|
39
|
+
|
|
40
|
+
# Поддержка разных форматов
|
|
41
|
+
if 'secrets' in data:
|
|
42
|
+
if isinstance(data['secrets'], list):
|
|
43
|
+
for item in data['secrets']:
|
|
44
|
+
if isinstance(item, str):
|
|
45
|
+
secrets[item] = ""
|
|
46
|
+
elif isinstance(item, dict):
|
|
47
|
+
secrets[item.get('secret', '')] = item.get('tag', '')
|
|
48
|
+
elif isinstance(data['secrets'], dict):
|
|
49
|
+
secrets = data['secrets']
|
|
50
|
+
|
|
51
|
+
return cls(
|
|
52
|
+
secrets=secrets,
|
|
53
|
+
port=data.get('port', 443),
|
|
54
|
+
enable_fake_tls=data.get('enable_fake_tls', True),
|
|
55
|
+
handshake_timeout=data.get('handshake_timeout', 10),
|
|
56
|
+
stats_interval=data.get('stats_interval', 60),
|
|
57
|
+
log_level=data.get('log_level', 'INFO'),
|
|
58
|
+
log_file=data.get('log_file'),
|
|
59
|
+
pid_file=data.get('pid_file')
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_file(cls, path: str) -> 'ProxyConfig':
|
|
64
|
+
"""Загрузка конфигурации из файла"""
|
|
65
|
+
if not os.path.exists(path):
|
|
66
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
|
67
|
+
|
|
68
|
+
with open(path, 'r') as f:
|
|
69
|
+
if path.endswith('.json'):
|
|
70
|
+
data = json.load(f)
|
|
71
|
+
else:
|
|
72
|
+
# INI-like format
|
|
73
|
+
data = cls._parse_ini(f.read())
|
|
74
|
+
|
|
75
|
+
return cls.from_dict(data)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def _parse_ini(cls, content: str) -> dict:
|
|
79
|
+
"""Парсинг INI-подобного формата"""
|
|
80
|
+
data = {'secrets': {}}
|
|
81
|
+
|
|
82
|
+
for line in content.split('\n'):
|
|
83
|
+
line = line.strip()
|
|
84
|
+
if not line or line.startswith('#'):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if '=' in line:
|
|
88
|
+
key, value = line.split('=', 1)
|
|
89
|
+
key = key.strip()
|
|
90
|
+
value = value.strip()
|
|
91
|
+
|
|
92
|
+
if key == 'secret':
|
|
93
|
+
# Простой формат: secret=xxx
|
|
94
|
+
data['secrets'][value] = ""
|
|
95
|
+
elif key.startswith('secret_'):
|
|
96
|
+
# Формат: secret_name=xxx
|
|
97
|
+
tag = key.replace('secret_', '')
|
|
98
|
+
data['secrets'][value] = tag
|
|
99
|
+
else:
|
|
100
|
+
# Другие параметры
|
|
101
|
+
data[key] = value
|
|
102
|
+
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
def save(self, path: str) -> None:
|
|
106
|
+
"""Сохранение конфигурации в файл"""
|
|
107
|
+
data = {
|
|
108
|
+
'port': self.port,
|
|
109
|
+
'enable_fake_tls': self.enable_fake_tls,
|
|
110
|
+
'handshake_timeout': self.handshake_timeout,
|
|
111
|
+
'stats_interval': self.stats_interval,
|
|
112
|
+
'log_level': self.log_level,
|
|
113
|
+
'secrets': self.secrets
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
with open(path, 'w') as f:
|
|
117
|
+
json.dump(data, f, indent=2)
|
|
118
|
+
|
|
119
|
+
def add_secret(self, secret: Optional[str] = None, tag: str = "") -> str:
|
|
120
|
+
"""Добавление нового секрета"""
|
|
121
|
+
if not secret:
|
|
122
|
+
secret = generate_secret()
|
|
123
|
+
|
|
124
|
+
self.secrets[secret] = tag
|
|
125
|
+
return secret
|
|
126
|
+
|
|
127
|
+
def remove_secret(self, secret: str) -> bool:
|
|
128
|
+
"""Удаление секрета"""
|
|
129
|
+
if secret in self.secrets:
|
|
130
|
+
del self.secrets[secret]
|
|
131
|
+
return True
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def verify_secret(self, data: bytes) -> Tuple[bool, Optional[str]]:
|
|
135
|
+
"""
|
|
136
|
+
Проверка handshake и определение использованного секрета
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Tuple[bool, Optional[str]]: (успех, секрет)
|
|
140
|
+
"""
|
|
141
|
+
for secret_hex, tag in self.secrets.items():
|
|
142
|
+
secret = bytes.fromhex(secret_hex)
|
|
143
|
+
if verify_fake_tls_handshake(data, secret):
|
|
144
|
+
return True, secret_hex
|
|
145
|
+
|
|
146
|
+
return False, None
|
|
147
|
+
|
|
148
|
+
def get_connection_links(self, base_ip: Optional[str] = None) -> Dict[str, str]:
|
|
149
|
+
"""Получение ссылок для всех секретов"""
|
|
150
|
+
from .utils import get_external_ip
|
|
151
|
+
|
|
152
|
+
ip = base_ip or get_external_ip() or "YOUR_SERVER_IP"
|
|
153
|
+
links = {}
|
|
154
|
+
|
|
155
|
+
for secret, tag in self.secrets.items():
|
|
156
|
+
link = f"tg://proxy?server={ip}&port={self.port}&secret={secret}"
|
|
157
|
+
if tag:
|
|
158
|
+
link += f"&tag={tag}"
|
|
159
|
+
links[secret[:16] + ("..." if len(secret) > 16 else "")] = link
|
|
160
|
+
|
|
161
|
+
return links
|
mtproxy/daemon.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Модуль для запуска как системный демон"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import atexit
|
|
6
|
+
import signal
|
|
7
|
+
import time
|
|
8
|
+
from typing import Optional, Callable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Daemon:
|
|
12
|
+
"""
|
|
13
|
+
Класс для управления демоном
|
|
14
|
+
|
|
15
|
+
Пример использования:
|
|
16
|
+
daemon = Daemon(pidfile='/var/run/mtproxy.pid')
|
|
17
|
+
daemon.start(run_function)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, pidfile: str, stdin: str = '/dev/null',
|
|
21
|
+
stdout: str = '/dev/null', stderr: str = '/dev/null'):
|
|
22
|
+
"""
|
|
23
|
+
Инициализация демона
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
pidfile: Путь к файлу PID
|
|
27
|
+
stdin: Перенаправление stdin
|
|
28
|
+
stdout: Перенаправление stdout
|
|
29
|
+
stderr: Перенаправление stderr
|
|
30
|
+
"""
|
|
31
|
+
self.pidfile = pidfile
|
|
32
|
+
self.stdin = stdin
|
|
33
|
+
self.stdout = stdout
|
|
34
|
+
self.stderr = stderr
|
|
35
|
+
self._running = False
|
|
36
|
+
|
|
37
|
+
def _daemonize(self) -> None:
|
|
38
|
+
"""Демонизация процесса (двойной fork)"""
|
|
39
|
+
try:
|
|
40
|
+
pid = os.fork()
|
|
41
|
+
if pid > 0:
|
|
42
|
+
sys.exit(0)
|
|
43
|
+
except OSError as e:
|
|
44
|
+
sys.stderr.write(f"Fork #1 failed: {e}\n")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
# Отсоединение от терминала
|
|
48
|
+
os.setsid()
|
|
49
|
+
os.umask(0)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
pid = os.fork()
|
|
53
|
+
if pid > 0:
|
|
54
|
+
sys.exit(0)
|
|
55
|
+
except OSError as e:
|
|
56
|
+
sys.stderr.write(f"Fork #2 failed: {e}\n")
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
# Перенаправление файловых дескрипторов
|
|
60
|
+
sys.stdout.flush()
|
|
61
|
+
sys.stderr.flush()
|
|
62
|
+
|
|
63
|
+
with open(self.stdin, 'r') as si:
|
|
64
|
+
os.dup2(si.fileno(), sys.stdin.fileno())
|
|
65
|
+
with open(self.stdout, 'a+') as so:
|
|
66
|
+
os.dup2(so.fileno(), sys.stdout.fileno())
|
|
67
|
+
with open(self.stderr, 'a+') as se:
|
|
68
|
+
os.dup2(se.fileno(), sys.stderr.fileno())
|
|
69
|
+
|
|
70
|
+
# Запись PID
|
|
71
|
+
self._write_pidfile()
|
|
72
|
+
|
|
73
|
+
# Регистрация удаления PID файла при выходе
|
|
74
|
+
atexit.register(self._remove_pidfile)
|
|
75
|
+
|
|
76
|
+
# Обработка сигналов
|
|
77
|
+
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
78
|
+
signal.signal(signal.SIGINT, self._signal_handler)
|
|
79
|
+
|
|
80
|
+
def _write_pidfile(self) -> None:
|
|
81
|
+
"""Запись PID в файл"""
|
|
82
|
+
with open(self.pidfile, 'w') as f:
|
|
83
|
+
f.write(str(os.getpid()))
|
|
84
|
+
|
|
85
|
+
def _remove_pidfile(self) -> None:
|
|
86
|
+
"""Удаление PID файла"""
|
|
87
|
+
if os.path.exists(self.pidfile):
|
|
88
|
+
os.remove(self.pidfile)
|
|
89
|
+
|
|
90
|
+
def _signal_handler(self, signum, frame) -> None:
|
|
91
|
+
"""Обработчик сигналов"""
|
|
92
|
+
self._running = False
|
|
93
|
+
|
|
94
|
+
def _get_pid(self) -> Optional[int]:
|
|
95
|
+
"""Получение PID из файла"""
|
|
96
|
+
try:
|
|
97
|
+
with open(self.pidfile, 'r') as f:
|
|
98
|
+
return int(f.read().strip())
|
|
99
|
+
except (IOError, ValueError):
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def start(self, run_func: Callable, *args, **kwargs) -> int:
|
|
103
|
+
"""
|
|
104
|
+
Запуск демона
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
run_func: Функция для запуска
|
|
108
|
+
*args, **kwargs: Аргументы для функции
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
int: 0 при успехе, 1 при ошибке
|
|
112
|
+
"""
|
|
113
|
+
pid = self._get_pid()
|
|
114
|
+
|
|
115
|
+
if pid:
|
|
116
|
+
# Проверка, работает ли процесс
|
|
117
|
+
try:
|
|
118
|
+
os.kill(pid, 0)
|
|
119
|
+
sys.stderr.write(f"Daemon already running with PID {pid}\n")
|
|
120
|
+
return 1
|
|
121
|
+
except OSError:
|
|
122
|
+
# Процесс не работает, удаляем старый PID файл
|
|
123
|
+
self._remove_pidfile()
|
|
124
|
+
|
|
125
|
+
# Демонизация
|
|
126
|
+
self._daemonize()
|
|
127
|
+
|
|
128
|
+
# Запуск основной функции
|
|
129
|
+
self._running = True
|
|
130
|
+
try:
|
|
131
|
+
run_func(*args, **kwargs)
|
|
132
|
+
except KeyboardInterrupt:
|
|
133
|
+
pass
|
|
134
|
+
except Exception as e:
|
|
135
|
+
sys.stderr.write(f"Error in daemon: {e}\n")
|
|
136
|
+
return 1
|
|
137
|
+
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
def stop(self) -> int:
|
|
141
|
+
"""Остановка демона"""
|
|
142
|
+
pid = self._get_pid()
|
|
143
|
+
|
|
144
|
+
if not pid:
|
|
145
|
+
sys.stderr.write("Daemon not running\n")
|
|
146
|
+
return 1
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
os.kill(pid, signal.SIGTERM)
|
|
150
|
+
|
|
151
|
+
# Ожидание завершения
|
|
152
|
+
timeout = 10
|
|
153
|
+
while timeout > 0:
|
|
154
|
+
try:
|
|
155
|
+
os.kill(pid, 0)
|
|
156
|
+
time.sleep(0.5)
|
|
157
|
+
timeout -= 0.5
|
|
158
|
+
except OSError:
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
# Принудительное завершение
|
|
162
|
+
try:
|
|
163
|
+
os.kill(pid, 0)
|
|
164
|
+
os.kill(pid, signal.SIGKILL)
|
|
165
|
+
except OSError:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
self._remove_pidfile()
|
|
169
|
+
|
|
170
|
+
except OSError as e:
|
|
171
|
+
sys.stderr.write(f"Error stopping daemon: {e}\n")
|
|
172
|
+
return 1
|
|
173
|
+
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
def restart(self, run_func: Callable, *args, **kwargs) -> int:
|
|
177
|
+
"""Перезапуск демона"""
|
|
178
|
+
self.stop()
|
|
179
|
+
return self.start(run_func, *args, **kwargs)
|
|
180
|
+
|
|
181
|
+
def status(self) -> str:
|
|
182
|
+
"""Проверка статуса демона"""
|
|
183
|
+
pid = self._get_pid()
|
|
184
|
+
|
|
185
|
+
if not pid:
|
|
186
|
+
return "stopped"
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
os.kill(pid, 0)
|
|
190
|
+
return f"running (PID: {pid})"
|
|
191
|
+
except OSError:
|
|
192
|
+
return "stopped (stale PID file)"
|
mtproxy/logger.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Модуль логирования"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from logging.handlers import RotatingFileHandler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ColoredFormatter(logging.Formatter):
|
|
10
|
+
"""Форматирование с цветами для консоли"""
|
|
11
|
+
|
|
12
|
+
COLORS = {
|
|
13
|
+
'DEBUG': '\033[36m', # Cyan
|
|
14
|
+
'INFO': '\033[32m', # Green
|
|
15
|
+
'WARNING': '\033[33m', # Yellow
|
|
16
|
+
'ERROR': '\033[31m', # Red
|
|
17
|
+
'CRITICAL': '\033[35m', # Magenta
|
|
18
|
+
}
|
|
19
|
+
RESET = '\033[0m'
|
|
20
|
+
|
|
21
|
+
def format(self, record):
|
|
22
|
+
levelname = record.levelname
|
|
23
|
+
if levelname in self.COLORS:
|
|
24
|
+
record.levelname = f"{self.COLORS[levelname]}{levelname}{self.RESET}"
|
|
25
|
+
return super().format(record)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def setup_logger(
|
|
29
|
+
name: str = "mtproxy",
|
|
30
|
+
level: str = "INFO",
|
|
31
|
+
log_file: Optional[str] = None,
|
|
32
|
+
console: bool = True,
|
|
33
|
+
max_bytes: int = 10 * 1024 * 1024, # 10 MB
|
|
34
|
+
backup_count: int = 5
|
|
35
|
+
) -> logging.Logger:
|
|
36
|
+
"""
|
|
37
|
+
Настройка логгера
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
name: Имя логгера
|
|
41
|
+
level: Уровень логирования (DEBUG, INFO, WARNING, ERROR)
|
|
42
|
+
log_file: Путь к файлу лога (опционально)
|
|
43
|
+
console: Вывод в консоль
|
|
44
|
+
max_bytes: Максимальный размер файла лога
|
|
45
|
+
backup_count: Количество резервных копий
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
logging.Logger: Настроенный логгер
|
|
49
|
+
"""
|
|
50
|
+
logger = logging.getLogger(name)
|
|
51
|
+
|
|
52
|
+
# Очистка существующих обработчиков
|
|
53
|
+
logger.handlers.clear()
|
|
54
|
+
|
|
55
|
+
# Установка уровня
|
|
56
|
+
level_map = {
|
|
57
|
+
'DEBUG': logging.DEBUG,
|
|
58
|
+
'INFO': logging.INFO,
|
|
59
|
+
'WARNING': logging.WARNING,
|
|
60
|
+
'ERROR': logging.ERROR,
|
|
61
|
+
'CRITICAL': logging.CRITICAL
|
|
62
|
+
}
|
|
63
|
+
logger.setLevel(level_map.get(level.upper(), logging.INFO))
|
|
64
|
+
|
|
65
|
+
# Формат логов
|
|
66
|
+
formatter = logging.Formatter(
|
|
67
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
68
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Консольный вывод
|
|
72
|
+
if console:
|
|
73
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
74
|
+
console_handler.setFormatter(ColoredFormatter(
|
|
75
|
+
'%(asctime)s - %(levelname)s - %(message)s',
|
|
76
|
+
datefmt='%H:%M:%S'
|
|
77
|
+
))
|
|
78
|
+
logger.addHandler(console_handler)
|
|
79
|
+
|
|
80
|
+
# Файловый вывод
|
|
81
|
+
if log_file:
|
|
82
|
+
file_handler = RotatingFileHandler(
|
|
83
|
+
log_file,
|
|
84
|
+
maxBytes=max_bytes,
|
|
85
|
+
backupCount=backup_count
|
|
86
|
+
)
|
|
87
|
+
file_handler.setFormatter(formatter)
|
|
88
|
+
logger.addHandler(file_handler)
|
|
89
|
+
|
|
90
|
+
return logger
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Глобальный логгер
|
|
94
|
+
_default_logger = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_logger() -> logging.Logger:
|
|
98
|
+
"""Получение глобального логгера"""
|
|
99
|
+
global _default_logger
|
|
100
|
+
if _default_logger is None:
|
|
101
|
+
_default_logger = setup_logger()
|
|
102
|
+
return _default_logger
|
mtproxy/server.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Основной класс MTProto прокси сервера"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import uuid
|
|
5
|
+
import signal
|
|
6
|
+
import time
|
|
7
|
+
from typing import Optional, Dict, Callable
|
|
8
|
+
from .config import ProxyConfig
|
|
9
|
+
from .stats import ProxyStats
|
|
10
|
+
from .logger import get_logger
|
|
11
|
+
from .utils import verify_fake_tls_handshake, get_external_ip
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MTProxyServer:
|
|
15
|
+
"""
|
|
16
|
+
MTProto прокси сервер с поддержкой нескольких секретов и статистики
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: ProxyConfig):
|
|
20
|
+
"""
|
|
21
|
+
Инициализация прокси сервера
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
config: Конфигурация прокси
|
|
25
|
+
"""
|
|
26
|
+
self.config = config
|
|
27
|
+
self.logger = get_logger()
|
|
28
|
+
self.stats = ProxyStats()
|
|
29
|
+
self.server: Optional[asyncio.Server] = None
|
|
30
|
+
self._running = False
|
|
31
|
+
self._tasks: Dict[str, asyncio.Task] = {}
|
|
32
|
+
self._conn_counter = 0
|
|
33
|
+
|
|
34
|
+
def _get_conn_id(self) -> str:
|
|
35
|
+
"""Генерация ID для соединения"""
|
|
36
|
+
self._conn_counter += 1
|
|
37
|
+
return f"{self._conn_counter}_{uuid.uuid4().hex[:8]}"
|
|
38
|
+
|
|
39
|
+
async def _handle_connection(
|
|
40
|
+
self,
|
|
41
|
+
reader: asyncio.StreamReader,
|
|
42
|
+
writer: asyncio.StreamWriter
|
|
43
|
+
):
|
|
44
|
+
"""Обработка входящего подключения"""
|
|
45
|
+
conn_id = self._get_conn_id()
|
|
46
|
+
client_addr = writer.get_extra_info('peername')
|
|
47
|
+
client_ip = client_addr[0] if client_addr else "unknown"
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Чтение handshake
|
|
51
|
+
handshake = await asyncio.wait_for(
|
|
52
|
+
reader.read(1024),
|
|
53
|
+
timeout=self.config.handshake_timeout
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if not handshake:
|
|
57
|
+
writer.close()
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Аутентификация с несколькими секретами
|
|
61
|
+
if self.config.enable_fake_tls and handshake[0] == 0x16:
|
|
62
|
+
success, secret = self.config.verify_secret(handshake)
|
|
63
|
+
if not success:
|
|
64
|
+
self.logger.warning(f"Auth failed from {client_ip}")
|
|
65
|
+
writer.close()
|
|
66
|
+
return
|
|
67
|
+
else:
|
|
68
|
+
# Классический режим - проверяем первый секрет
|
|
69
|
+
if len(handshake) < 4 or handshake[:4] != b'\xef\xef\xef\xef':
|
|
70
|
+
self.logger.warning(f"Invalid protocol from {client_ip}")
|
|
71
|
+
writer.close()
|
|
72
|
+
return
|
|
73
|
+
secret = list(self.config.secrets.keys())[0] if self.config.secrets else None
|
|
74
|
+
if not secret:
|
|
75
|
+
writer.close()
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
self.logger.info(f"Client connected: {client_ip} (secret: {secret[:16]}...)")
|
|
79
|
+
|
|
80
|
+
# Регистрация соединения в статистике
|
|
81
|
+
self.stats.connection_started(conn_id, client_ip, secret)
|
|
82
|
+
|
|
83
|
+
# Подключение к Telegram
|
|
84
|
+
# Используем несколько IP для балансировки
|
|
85
|
+
tg_ips = ['149.154.167.50', '149.154.167.51', '149.154.175.100']
|
|
86
|
+
tg_reader, tg_writer = await asyncio.open_connection(
|
|
87
|
+
tg_ips[hash(secret) % len(tg_ips)],
|
|
88
|
+
443
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Создание задач для пересылки данных
|
|
92
|
+
task_client = asyncio.create_task(
|
|
93
|
+
self._forward(reader, tg_writer, conn_id, sent=True)
|
|
94
|
+
)
|
|
95
|
+
task_server = asyncio.create_task(
|
|
96
|
+
self._forward(tg_reader, writer, conn_id, received=True)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self._tasks[conn_id] = task_client
|
|
100
|
+
self._tasks[f"{conn_id}_server"] = task_server
|
|
101
|
+
|
|
102
|
+
# Ожидание завершения
|
|
103
|
+
await asyncio.gather(task_client, task_server)
|
|
104
|
+
|
|
105
|
+
except asyncio.TimeoutError:
|
|
106
|
+
self.logger.debug(f"Handshake timeout from {client_ip}")
|
|
107
|
+
except ConnectionRefusedError:
|
|
108
|
+
self.logger.error(f"Connection refused to Telegram from {client_ip}")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self.logger.error(f"Error handling connection from {client_ip}: {e}")
|
|
111
|
+
finally:
|
|
112
|
+
# Завершение соединения
|
|
113
|
+
self.stats.connection_ended(conn_id)
|
|
114
|
+
|
|
115
|
+
# Удаление задач
|
|
116
|
+
for task_id in [conn_id, f"{conn_id}_server"]:
|
|
117
|
+
if task_id in self._tasks:
|
|
118
|
+
self._tasks[task_id].cancel()
|
|
119
|
+
del self._tasks[task_id]
|
|
120
|
+
|
|
121
|
+
writer.close()
|
|
122
|
+
await writer.wait_closed()
|
|
123
|
+
|
|
124
|
+
self.logger.debug(f"Connection closed: {client_ip}")
|
|
125
|
+
|
|
126
|
+
async def _forward(
|
|
127
|
+
self,
|
|
128
|
+
reader: asyncio.StreamReader,
|
|
129
|
+
writer: asyncio.StreamWriter,
|
|
130
|
+
conn_id: str,
|
|
131
|
+
sent: bool = False,
|
|
132
|
+
received: bool = False
|
|
133
|
+
):
|
|
134
|
+
"""Пересылка данных с обновлением статистики"""
|
|
135
|
+
try:
|
|
136
|
+
while True:
|
|
137
|
+
data = await reader.read(8192)
|
|
138
|
+
if not data:
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
writer.write(data)
|
|
142
|
+
await writer.drain()
|
|
143
|
+
|
|
144
|
+
# Обновление статистики
|
|
145
|
+
if sent:
|
|
146
|
+
self.stats.update_bytes(conn_id, sent=len(data))
|
|
147
|
+
if received:
|
|
148
|
+
self.stats.update_bytes(conn_id, received=len(data))
|
|
149
|
+
|
|
150
|
+
except (asyncio.CancelledError, ConnectionError):
|
|
151
|
+
pass
|
|
152
|
+
except Exception as e:
|
|
153
|
+
self.logger.debug(f"Forward error: {e}")
|
|
154
|
+
finally:
|
|
155
|
+
writer.close()
|
|
156
|
+
|
|
157
|
+
async def _stats_reporter(self):
|
|
158
|
+
"""Периодический вывод статистики"""
|
|
159
|
+
while self._running:
|
|
160
|
+
await asyncio.sleep(self.config.stats_interval)
|
|
161
|
+
self.logger.info("\n" + self.stats.format_summary())
|
|
162
|
+
|
|
163
|
+
def start(self):
|
|
164
|
+
"""Запуск прокси сервера"""
|
|
165
|
+
loop = asyncio.new_event_loop()
|
|
166
|
+
asyncio.set_event_loop(loop)
|
|
167
|
+
|
|
168
|
+
self._running = True
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
# Запуск сервера
|
|
172
|
+
self.server = loop.run_until_complete(
|
|
173
|
+
asyncio.start_server(
|
|
174
|
+
self._handle_connection,
|
|
175
|
+
'0.0.0.0',
|
|
176
|
+
self.config.port
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Запуск репортера статистики
|
|
181
|
+
stats_task = loop.create_task(self._stats_reporter())
|
|
182
|
+
|
|
183
|
+
# Вывод информации
|
|
184
|
+
print("=" * 60)
|
|
185
|
+
print("🚀 MTProxy Server Started")
|
|
186
|
+
print("=" * 60)
|
|
187
|
+
print(f"📡 Port: {self.config.port}")
|
|
188
|
+
print(f"🔐 Fake TLS: {self.config.enable_fake_tls}")
|
|
189
|
+
print(f"🔑 Secrets count: {len(self.config.secrets)}")
|
|
190
|
+
print(f"📊 Stats interval: {self.config.stats_interval}s")
|
|
191
|
+
print("-" * 60)
|
|
192
|
+
print("Connection links:")
|
|
193
|
+
|
|
194
|
+
ip = get_external_ip()
|
|
195
|
+
for secret, tag in self.config.secrets.items():
|
|
196
|
+
link = f"tg://proxy?server={ip}&port={self.config.port}&secret={secret}"
|
|
197
|
+
if tag:
|
|
198
|
+
link += f"&tag={tag}"
|
|
199
|
+
print(f" {link}")
|
|
200
|
+
|
|
201
|
+
print("=" * 60)
|
|
202
|
+
print("Press Ctrl+C to stop")
|
|
203
|
+
print("=" * 60)
|
|
204
|
+
|
|
205
|
+
# Обработка сигналов
|
|
206
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
207
|
+
loop.add_signal_handler(sig, lambda: asyncio.create_task(self.stop()))
|
|
208
|
+
|
|
209
|
+
loop.run_forever()
|
|
210
|
+
|
|
211
|
+
except KeyboardInterrupt:
|
|
212
|
+
pass
|
|
213
|
+
except Exception as e:
|
|
214
|
+
self.logger.error(f"Server error: {e}")
|
|
215
|
+
finally:
|
|
216
|
+
stats_task.cancel()
|
|
217
|
+
self._running = False
|
|
218
|
+
if self.server:
|
|
219
|
+
self.server.close()
|
|
220
|
+
loop.close()
|
|
221
|
+
|
|
222
|
+
async def stop(self):
|
|
223
|
+
"""Остановка сервера"""
|
|
224
|
+
self.logger.info("Shutting down...")
|
|
225
|
+
self._running = False
|
|
226
|
+
|
|
227
|
+
# Отмена всех задач
|
|
228
|
+
for task in self._tasks.values():
|
|
229
|
+
task.cancel()
|
|
230
|
+
|
|
231
|
+
if self.server:
|
|
232
|
+
self.server.close()
|
|
233
|
+
await self.server.wait_closed()
|
|
234
|
+
|
|
235
|
+
self.logger.info("Server stopped")
|
mtproxy/stats.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Модуль статистики"""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ConnectionStats:
|
|
13
|
+
"""Статистика по одному соединению"""
|
|
14
|
+
client_ip: str
|
|
15
|
+
secret_key: str
|
|
16
|
+
start_time: float
|
|
17
|
+
bytes_sent: int = 0
|
|
18
|
+
bytes_received: int = 0
|
|
19
|
+
end_time: Optional[float] = None
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def duration(self) -> float:
|
|
23
|
+
"""Длительность соединения в секундах"""
|
|
24
|
+
end = self.end_time or time.time()
|
|
25
|
+
return end - self.start_time
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def total_bytes(self) -> int:
|
|
29
|
+
"""Общее количество байт"""
|
|
30
|
+
return self.bytes_sent + self.bytes_received
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ProxyStats:
|
|
34
|
+
"""
|
|
35
|
+
Сбор статистики прокси сервера
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
total_connections (int): Всего подключений
|
|
39
|
+
active_connections (int): Активных подключений
|
|
40
|
+
total_bytes_sent (int): Всего отправлено байт
|
|
41
|
+
total_bytes_received (int): Всего получено байт
|
|
42
|
+
connections_per_secret (dict): Статистика по секретам
|
|
43
|
+
connections_per_ip (dict): Статистика по IP
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self._lock = threading.Lock()
|
|
48
|
+
self._connections: Dict[str, ConnectionStats] = {}
|
|
49
|
+
self._start_time = time.time()
|
|
50
|
+
|
|
51
|
+
# Общая статистика
|
|
52
|
+
self.total_connections = 0
|
|
53
|
+
self.active_connections = 0
|
|
54
|
+
self.total_bytes_sent = 0
|
|
55
|
+
self.total_bytes_received = 0
|
|
56
|
+
|
|
57
|
+
# Детальная статистика
|
|
58
|
+
self.connections_per_secret: Dict[str, int] = defaultdict(int)
|
|
59
|
+
self.connections_per_ip: Dict[str, int] = defaultdict(int)
|
|
60
|
+
self.bytes_per_secret: Dict[str, Dict[str, int]] = defaultdict(
|
|
61
|
+
lambda: {'sent': 0, 'received': 0}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def connection_started(self, conn_id: str, client_ip: str, secret_key: str) -> None:
|
|
65
|
+
"""Начало соединения"""
|
|
66
|
+
with self._lock:
|
|
67
|
+
self._connections[conn_id] = ConnectionStats(
|
|
68
|
+
client_ip=client_ip,
|
|
69
|
+
secret_key=secret_key,
|
|
70
|
+
start_time=time.time()
|
|
71
|
+
)
|
|
72
|
+
self.total_connections += 1
|
|
73
|
+
self.active_connections += 1
|
|
74
|
+
self.connections_per_secret[secret_key] += 1
|
|
75
|
+
self.connections_per_ip[client_ip] += 1
|
|
76
|
+
|
|
77
|
+
def connection_ended(self, conn_id: str) -> Optional[ConnectionStats]:
|
|
78
|
+
"""Завершение соединения"""
|
|
79
|
+
with self._lock:
|
|
80
|
+
if conn_id not in self._connections:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
conn = self._connections[conn_id]
|
|
84
|
+
conn.end_time = time.time()
|
|
85
|
+
|
|
86
|
+
self.active_connections -= 1
|
|
87
|
+
self.total_bytes_sent += conn.bytes_sent
|
|
88
|
+
self.total_bytes_received += conn.bytes_received
|
|
89
|
+
|
|
90
|
+
# Обновление статистики по секрету
|
|
91
|
+
self.bytes_per_secret[conn.secret_key]['sent'] += conn.bytes_sent
|
|
92
|
+
self.bytes_per_secret[conn.secret_key]['received'] += conn.bytes_received
|
|
93
|
+
|
|
94
|
+
return self._connections.pop(conn_id, None)
|
|
95
|
+
|
|
96
|
+
def update_bytes(self, conn_id: str, sent: int = 0, received: int = 0) -> None:
|
|
97
|
+
"""Обновление счетчиков байт"""
|
|
98
|
+
with self._lock:
|
|
99
|
+
if conn_id in self._connections:
|
|
100
|
+
self._connections[conn_id].bytes_sent += sent
|
|
101
|
+
self._connections[conn_id].bytes_received += received
|
|
102
|
+
|
|
103
|
+
def get_summary(self) -> dict:
|
|
104
|
+
"""Получение сводной статистики"""
|
|
105
|
+
with self._lock:
|
|
106
|
+
uptime = time.time() - self._start_time
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
'uptime_seconds': uptime,
|
|
110
|
+
'total_connections': self.total_connections,
|
|
111
|
+
'active_connections': self.active_connections,
|
|
112
|
+
'total_bytes_sent': self.total_bytes_sent,
|
|
113
|
+
'total_bytes_received': self.total_bytes_received,
|
|
114
|
+
'total_bytes': self.total_bytes_sent + self.total_bytes_received,
|
|
115
|
+
'connections_per_second': self.total_connections / uptime if uptime > 0 else 0,
|
|
116
|
+
'bytes_per_second': (self.total_bytes_sent + self.total_bytes_received) / uptime if uptime > 0 else 0,
|
|
117
|
+
'secrets': dict(self.connections_per_secret),
|
|
118
|
+
'bytes_per_secret': dict(self.bytes_per_secret),
|
|
119
|
+
'top_ips': sorted(
|
|
120
|
+
self.connections_per_ip.items(),
|
|
121
|
+
key=lambda x: x[1],
|
|
122
|
+
reverse=True
|
|
123
|
+
)[:10]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def format_summary(self) -> str:
|
|
127
|
+
"""Форматирование сводки для вывода"""
|
|
128
|
+
stats = self.get_summary()
|
|
129
|
+
|
|
130
|
+
def format_bytes(b: int) -> str:
|
|
131
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
132
|
+
if b < 1024:
|
|
133
|
+
return f"{b:.2f} {unit}"
|
|
134
|
+
b /= 1024
|
|
135
|
+
return f"{b:.2f} TB"
|
|
136
|
+
|
|
137
|
+
lines = [
|
|
138
|
+
"=" * 50,
|
|
139
|
+
"MTProxy Statistics",
|
|
140
|
+
"=" * 50,
|
|
141
|
+
f"Uptime: {stats['uptime_seconds']:.0f} seconds",
|
|
142
|
+
f"Total connections: {stats['total_connections']}",
|
|
143
|
+
f"Active connections: {stats['active_connections']}",
|
|
144
|
+
f"Total traffic: {format_bytes(stats['total_bytes'])}",
|
|
145
|
+
f" - Sent: {format_bytes(stats['total_bytes_sent'])}",
|
|
146
|
+
f" - Received: {format_bytes(stats['total_bytes_received'])}",
|
|
147
|
+
f"Connections/sec: {stats['connections_per_second']:.2f}",
|
|
148
|
+
f"Traffic/sec: {format_bytes(stats['bytes_per_second'])}",
|
|
149
|
+
"-" * 50,
|
|
150
|
+
"Statistics by secret:",
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
for secret, count in stats['secrets'].items():
|
|
154
|
+
bytes_stats = stats['bytes_per_secret'].get(secret, {'sent': 0, 'received': 0})
|
|
155
|
+
total = bytes_stats['sent'] + bytes_stats['received']
|
|
156
|
+
lines.append(
|
|
157
|
+
f" {secret[:16]}...: {count} connections, {format_bytes(total)}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if stats['top_ips']:
|
|
161
|
+
lines.append("-" * 50)
|
|
162
|
+
lines.append("Top 10 IPs:")
|
|
163
|
+
for ip, count in stats['top_ips']:
|
|
164
|
+
lines.append(f" {ip}: {count} connections")
|
|
165
|
+
|
|
166
|
+
lines.append("=" * 50)
|
|
167
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-mtproxy-lib
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Production-ready MTProto proxy server library with multi-secret support, statistics, and daemon mode
|
|
5
|
+
Home-page: https://github.com/twosleepynights0x1/py-mtproxy-lib
|
|
6
|
+
Author: Your Name
|
|
7
|
+
Author-email: tashova28@gmail.com
|
|
8
|
+
Project-URL: Bug Reports, https://github.com/twosleepynights0x1/py-mtproxy-lib
|
|
9
|
+
Keywords: mtproto telegram proxy vpn daemon docker
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: POSIX
|
|
21
|
+
Classifier: Operating System :: Unix
|
|
22
|
+
Requires-Python: >=3.7
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: pycryptodome>=3.10.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
28
|
+
Requires-Dist: black>=22.0; extra == "dev"
|
|
29
|
+
Requires-Dist: flake8>=4.0; extra == "dev"
|
|
30
|
+
Provides-Extra: docker
|
|
31
|
+
Requires-Dist: docker>=6.0; extra == "docker"
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: classifier
|
|
35
|
+
Dynamic: description
|
|
36
|
+
Dynamic: description-content-type
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: project-url
|
|
40
|
+
Dynamic: provides-extra
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
44
|
+
|
|
45
|
+
# py-mtproxy-lib
|
|
46
|
+
|
|
47
|
+
Python библиотека для запуска MTProto прокси сервера.
|
|
48
|
+
|
|
49
|
+
## Установка
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install py-mtproxy-lib
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
mtproxy/__init__.py,sha256=9-Zi4TGcbvUxqKgNEaEqbGuZkSRgKV5-W0hsbP8Xaf4,534
|
|
2
|
+
mtproxy/cli.py,sha256=TE5wm94dvR_lGd8gVNnG37aIuBx-QlB7TMloSTp4FDo,5238
|
|
3
|
+
mtproxy/config.py,sha256=CP8_8vpd27hCphfxAlT1uRlbXMHhSgi4c2m3qCVLCoc,6163
|
|
4
|
+
mtproxy/daemon.py,sha256=6jTfTD_SX5JGOSamBj8vQubnWPyAmBSv7l4tCQ6FRVc,6142
|
|
5
|
+
mtproxy/logger.py,sha256=vD24MnplywJtPVPkmdRXjZTL4uOeXzy2HEmVYH-afqs,3151
|
|
6
|
+
mtproxy/server.py,sha256=jtbwgItjv9upMC6bJMutmumCOSjeZeGyhsLgqCsQt_o,8978
|
|
7
|
+
mtproxy/stats.py,sha256=Py4GvrVmJIqHlVDAYq0fP-J7KmO4PI4BZMBmci2R1mA,6843
|
|
8
|
+
py_mtproxy_lib-0.0.1.dist-info/METADATA,sha256=OwZskjBISvQtk5jhk7F_AYeJpvREMoezN9y3V6OnalY,1794
|
|
9
|
+
py_mtproxy_lib-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
py_mtproxy_lib-0.0.1.dist-info/entry_points.txt,sha256=e0nc73vMvyxlBqZe6oWveRE26zl3emQWvKTgJ80Oqx0,45
|
|
11
|
+
py_mtproxy_lib-0.0.1.dist-info/top_level.txt,sha256=rOXQuaO2tvbsxQIRbvoBcoDB9wAOMMuvGDg24eYPqhc,8
|
|
12
|
+
py_mtproxy_lib-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mtproxy
|