py-mtproxy-lib 0.0.2__tar.gz → 0.0.4__tar.gz

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.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-mtproxy-lib
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Production-ready MTProto proxy server library with multi-secret support, statistics, and daemon mode
5
5
  Home-page: https://github.com/twosleepynights0x1/py-mtproxy-lib
6
- Author: Your Name
6
+ Author: AneK
7
7
  Author-email: tashova28@gmail.com
8
8
  Project-URL: Bug Reports, https://github.com/twosleepynights0x1/py-mtproxy-lib
9
9
  Keywords: mtproto telegram proxy vpn daemon docker
@@ -1,18 +1,14 @@
1
- """CLI интерфейс"""
2
-
3
1
  import argparse
4
2
  import sys
5
3
  import os
6
4
  from .config import ProxyConfig
7
5
  from .server import MTProxyServer
8
- from .utils import generate_secret
6
+ from .utils import generate_secret, find_free_port, get_public_ip, get_common_ports
9
7
  from .daemon import Daemon
10
8
  from .logger import setup_logger
11
9
 
12
10
 
13
11
  def start_server(config: ProxyConfig, foreground: bool = False):
14
- """Запуск сервера"""
15
- # Настройка логгера
16
12
  logger = setup_logger(
17
13
  level=config.log_level,
18
14
  log_file=config.log_file
@@ -32,7 +28,6 @@ def start_server(config: ProxyConfig, foreground: bool = False):
32
28
 
33
29
 
34
30
  def main():
35
- """Основная функция CLI"""
36
31
  parser = argparse.ArgumentParser(
37
32
  description="MTProto Proxy Server",
38
33
  prog="mtproxy"
@@ -53,10 +48,16 @@ def main():
53
48
 
54
49
  parser.add_argument(
55
50
  "-p", "--port",
56
- help="Port to listen on",
51
+ help="Port to listen on (auto-find if not specified)",
57
52
  type=int
58
53
  )
59
54
 
55
+ parser.add_argument(
56
+ "--auto-port",
57
+ help="Automatically find free port",
58
+ action="store_true"
59
+ )
60
+
60
61
  parser.add_argument(
61
62
  "--no-fake-tls",
62
63
  help="Disable Fake TLS masking",
@@ -106,9 +107,21 @@ def main():
106
107
  action="store_true"
107
108
  )
108
109
 
109
- # Команды управления демоном
110
- subparsers = parser.add_subparsers(dest="command", help="Daemon commands")
110
+ parser.add_argument(
111
+ "--show-ip",
112
+ help="Show public IP address",
113
+ action="store_true"
114
+ )
111
115
 
116
+ parser.add_argument(
117
+ "--find-port",
118
+ help="Find free port in range",
119
+ type=str,
120
+ nargs='?',
121
+ const="5000-10000"
122
+ )
123
+
124
+ subparsers = parser.add_subparsers(dest="command", help="Daemon commands")
112
125
  subparsers.add_parser("start", help="Start daemon")
113
126
  subparsers.add_parser("stop", help="Stop daemon")
114
127
  subparsers.add_parser("restart", help="Restart daemon")
@@ -116,12 +129,27 @@ def main():
116
129
 
117
130
  args = parser.parse_args()
118
131
 
119
- # Генерация секрета
120
132
  if args.generate_secret:
121
133
  print(generate_secret())
122
134
  sys.exit(0)
123
135
 
124
- # Загрузка конфигурации
136
+ if args.show_ip:
137
+ ip = get_public_ip()
138
+ print(ip if ip else "Could not detect public IP")
139
+ sys.exit(0)
140
+
141
+ if args.find_port:
142
+ try:
143
+ if '-' in args.find_port:
144
+ start, end = map(int, args.find_port.split('-'))
145
+ else:
146
+ start, end = 5000, 10000
147
+ port = find_free_port(start, end)
148
+ print(f"Free port found: {port}")
149
+ except Exception as e:
150
+ print(f"Error: {e}")
151
+ sys.exit(0)
152
+
125
153
  config = None
126
154
  if os.path.exists(args.config):
127
155
  try:
@@ -132,9 +160,16 @@ def main():
132
160
  if not config:
133
161
  config = ProxyConfig()
134
162
 
135
- # Применение CLI аргументов
136
163
  if args.port:
137
164
  config.port = args.port
165
+ elif args.auto_port or not config.port:
166
+ try:
167
+ config.port = find_free_port(5000, 10000, get_common_ports())
168
+ print(f"Auto-selected free port: {config.port}")
169
+ except Exception as e:
170
+ print(f"Error finding free port: {e}", file=sys.stderr)
171
+ sys.exit(1)
172
+
138
173
  if args.no_fake_tls:
139
174
  config.enable_fake_tls = False
140
175
  if args.pid_file:
@@ -146,25 +181,27 @@ def main():
146
181
  if args.stats_interval:
147
182
  config.stats_interval = args.stats_interval
148
183
 
149
- # Добавление секретов
150
184
  if args.secret:
151
185
  for secret in args.secret:
152
186
  config.add_secret(secret)
153
187
 
154
- # Если нет секретов, создаем один
155
188
  if not config.secrets:
156
189
  config.add_secret()
157
190
  print(f"Generated default secret: {list(config.secrets.keys())[0]}")
158
191
 
159
- # Показ ссылок
160
192
  if args.show_links:
161
- links = config.get_connection_links()
193
+ from .utils import get_public_ip
194
+ ip = get_public_ip() or "YOUR_SERVER_IP"
195
+ print(f"Server IP: {ip}")
196
+ print(f"Port: {config.port}")
162
197
  print("Connection links:")
163
- for name, link in links.items():
164
- print(f" {name}: {link}")
198
+ for secret, tag in config.secrets.items():
199
+ link = f"tg://proxy?server={ip}&port={config.port}&secret={secret}"
200
+ if tag:
201
+ link += f"&tag={tag}"
202
+ print(f" {link}")
165
203
  sys.exit(0)
166
204
 
167
- # Команды демона
168
205
  daemon = Daemon(
169
206
  pidfile=config.pid_file or '/var/run/mtproxy.pid',
170
207
  stdout=config.log_file or '/dev/null',
@@ -181,7 +218,6 @@ def main():
181
218
  print(daemon.status())
182
219
  sys.exit(0)
183
220
 
184
- # Запуск в foreground или daemon
185
221
  if args.daemon:
186
222
  daemon.start(start_server, config)
187
223
  else:
@@ -1,28 +1,17 @@
1
- """Основной класс MTProto прокси сервера"""
2
-
3
1
  import asyncio
4
2
  import uuid
5
3
  import signal
6
4
  import time
7
- from typing import Optional, Dict, Callable
5
+ from typing import Optional, Dict
8
6
  from .config import ProxyConfig
9
7
  from .stats import ProxyStats
10
8
  from .logger import get_logger
11
- from .utils import verify_fake_tls_handshake, get_external_ip
9
+ from .utils import get_external_ip, get_public_ip
12
10
 
13
11
 
14
12
  class MTProxyServer:
15
- """
16
- MTProto прокси сервер с поддержкой нескольких секретов и статистики
17
- """
18
13
 
19
14
  def __init__(self, config: ProxyConfig):
20
- """
21
- Инициализация прокси сервера
22
-
23
- Args:
24
- config: Конфигурация прокси
25
- """
26
15
  self.config = config
27
16
  self.logger = get_logger()
28
17
  self.stats = ProxyStats()
@@ -32,22 +21,30 @@ class MTProxyServer:
32
21
  self._conn_counter = 0
33
22
 
34
23
  def _get_conn_id(self) -> str:
35
- """Генерация ID для соединения"""
36
24
  self._conn_counter += 1
37
25
  return f"{self._conn_counter}_{uuid.uuid4().hex[:8]}"
38
26
 
39
- async def _handle_connection(
40
- self,
41
- reader: asyncio.StreamReader,
42
- writer: asyncio.StreamWriter
43
- ):
44
- """Обработка входящего подключения"""
27
+ def get_connection_link(self, custom_ip: str = None) -> str:
28
+ ip = custom_ip or get_public_ip() or get_external_ip() or "YOUR_SERVER_IP"
29
+ secret = list(self.config.secrets.keys())[0] if self.config.secrets else ""
30
+ return f"tg://proxy?server={ip}&port={self.config.port}&secret={secret}"
31
+
32
+ def get_all_links(self, custom_ip: str = None) -> Dict[str, str]:
33
+ ip = custom_ip or get_public_ip() or get_external_ip() or "YOUR_SERVER_IP"
34
+ links = {}
35
+ for secret, tag in self.config.secrets.items():
36
+ link = f"tg://proxy?server={ip}&port={self.config.port}&secret={secret}"
37
+ if tag:
38
+ link += f"&tag={tag}"
39
+ links[secret[:16] + ("..." if len(secret) > 16 else "")] = link
40
+ return links
41
+
42
+ async def _handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
45
43
  conn_id = self._get_conn_id()
46
44
  client_addr = writer.get_extra_info('peername')
47
45
  client_ip = client_addr[0] if client_addr else "unknown"
48
46
 
49
47
  try:
50
- # Чтение handshake
51
48
  handshake = await asyncio.wait_for(
52
49
  reader.read(1024),
53
50
  timeout=self.config.handshake_timeout
@@ -57,15 +54,20 @@ class MTProxyServer:
57
54
  writer.close()
58
55
  return
59
56
 
60
- # Аутентификация с несколькими секретами
61
57
  if self.config.enable_fake_tls and handshake[0] == 0x16:
62
- success, secret = self.config.verify_secret(handshake)
58
+ from .utils import verify_fake_tls_handshake
59
+ success, secret = False, None
60
+ for secret_hex, tag in self.config.secrets.items():
61
+ secret_bytes = bytes.fromhex(secret_hex)
62
+ if verify_fake_tls_handshake(handshake, secret_bytes):
63
+ success = True
64
+ secret = secret_hex
65
+ break
63
66
  if not success:
64
67
  self.logger.warning(f"Auth failed from {client_ip}")
65
68
  writer.close()
66
69
  return
67
70
  else:
68
- # Классический режим - проверяем первый секрет
69
71
  if len(handshake) < 4 or handshake[:4] != b'\xef\xef\xef\xef':
70
72
  self.logger.warning(f"Invalid protocol from {client_ip}")
71
73
  writer.close()
@@ -76,19 +78,14 @@ class MTProxyServer:
76
78
  return
77
79
 
78
80
  self.logger.info(f"Client connected: {client_ip} (secret: {secret[:16]}...)")
79
-
80
- # Регистрация соединения в статистике
81
81
  self.stats.connection_started(conn_id, client_ip, secret)
82
82
 
83
- # Подключение к Telegram
84
- # Используем несколько IP для балансировки
85
83
  tg_ips = ['149.154.167.50', '149.154.167.51', '149.154.175.100']
86
84
  tg_reader, tg_writer = await asyncio.open_connection(
87
85
  tg_ips[hash(secret) % len(tg_ips)],
88
86
  443
89
87
  )
90
88
 
91
- # Создание задач для пересылки данных
92
89
  task_client = asyncio.create_task(
93
90
  self._forward(reader, tg_writer, conn_id, sent=True)
94
91
  )
@@ -99,7 +96,6 @@ class MTProxyServer:
99
96
  self._tasks[conn_id] = task_client
100
97
  self._tasks[f"{conn_id}_server"] = task_server
101
98
 
102
- # Ожидание завершения
103
99
  await asyncio.gather(task_client, task_server)
104
100
 
105
101
  except asyncio.TimeoutError:
@@ -109,10 +105,8 @@ class MTProxyServer:
109
105
  except Exception as e:
110
106
  self.logger.error(f"Error handling connection from {client_ip}: {e}")
111
107
  finally:
112
- # Завершение соединения
113
108
  self.stats.connection_ended(conn_id)
114
109
 
115
- # Удаление задач
116
110
  for task_id in [conn_id, f"{conn_id}_server"]:
117
111
  if task_id in self._tasks:
118
112
  self._tasks[task_id].cancel()
@@ -120,18 +114,10 @@ class MTProxyServer:
120
114
 
121
115
  writer.close()
122
116
  await writer.wait_closed()
123
-
124
117
  self.logger.debug(f"Connection closed: {client_ip}")
125
118
 
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
- """Пересылка данных с обновлением статистики"""
119
+ async def _forward(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter,
120
+ conn_id: str, sent: bool = False, received: bool = False):
135
121
  try:
136
122
  while True:
137
123
  data = await reader.read(8192)
@@ -141,7 +127,6 @@ class MTProxyServer:
141
127
  writer.write(data)
142
128
  await writer.drain()
143
129
 
144
- # Обновление статистики
145
130
  if sent:
146
131
  self.stats.update_bytes(conn_id, sent=len(data))
147
132
  if received:
@@ -155,20 +140,17 @@ class MTProxyServer:
155
140
  writer.close()
156
141
 
157
142
  async def _stats_reporter(self):
158
- """Периодический вывод статистики"""
159
143
  while self._running:
160
144
  await asyncio.sleep(self.config.stats_interval)
161
145
  self.logger.info("\n" + self.stats.format_summary())
162
146
 
163
147
  def start(self):
164
- """Запуск прокси сервера"""
165
148
  loop = asyncio.new_event_loop()
166
149
  asyncio.set_event_loop(loop)
167
150
 
168
151
  self._running = True
169
152
 
170
153
  try:
171
- # Запуск сервера
172
154
  self.server = loop.run_until_complete(
173
155
  asyncio.start_server(
174
156
  self._handle_connection,
@@ -177,10 +159,8 @@ class MTProxyServer:
177
159
  )
178
160
  )
179
161
 
180
- # Запуск репортера статистики
181
162
  stats_task = loop.create_task(self._stats_reporter())
182
163
 
183
- # Вывод информации
184
164
  print("=" * 60)
185
165
  print("🚀 MTProxy Server Started")
186
166
  print("=" * 60)
@@ -191,18 +171,14 @@ class MTProxyServer:
191
171
  print("-" * 60)
192
172
  print("Connection links:")
193
173
 
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}"
174
+ links = self.get_all_links()
175
+ for name, link in links.items():
199
176
  print(f" {link}")
200
177
 
201
178
  print("=" * 60)
202
179
  print("Press Ctrl+C to stop")
203
180
  print("=" * 60)
204
181
 
205
- # Обработка сигналов
206
182
  for sig in (signal.SIGINT, signal.SIGTERM):
207
183
  loop.add_signal_handler(sig, lambda: asyncio.create_task(self.stop()))
208
184
 
@@ -220,11 +196,9 @@ class MTProxyServer:
220
196
  loop.close()
221
197
 
222
198
  async def stop(self):
223
- """Остановка сервера"""
224
199
  self.logger.info("Shutting down...")
225
200
  self._running = False
226
201
 
227
- # Отмена всех задач
228
202
  for task in self._tasks.values():
229
203
  task.cancel()
230
204
 
@@ -0,0 +1,96 @@
1
+ """Вспомогательные функции"""
2
+
3
+ import secrets
4
+ import hashlib
5
+ import hmac
6
+ from typing import Optional, List
7
+ import urllib.request
8
+ import socket
9
+
10
+
11
+ def generate_secret() -> str:
12
+ return secrets.token_hex(16)
13
+
14
+
15
+ def verify_fake_tls_handshake(data: bytes, secret: bytes) -> bool:
16
+ if not data or len(data) < 51 or data[0] != 0x16:
17
+ return False
18
+
19
+ digest = data[11:43]
20
+ timestamp = data[43:51]
21
+ expected = hmac.new(secret, timestamp, hashlib.sha256).digest()[:32]
22
+
23
+ return hmac.compare_digest(digest, expected)
24
+
25
+
26
+ def get_external_ip() -> Optional[str]:
27
+ try:
28
+ with urllib.request.urlopen('https://ifconfig.me', timeout=5) as response:
29
+ return response.read().decode('utf-8').strip()
30
+ except Exception:
31
+ return None
32
+
33
+
34
+ def find_free_port(start_port: int = 5000, end_port: int = 10000, exclude_ports: List[int] = None) -> int:
35
+ if exclude_ports is None:
36
+ exclude_ports = []
37
+
38
+ for port in range(start_port, end_port + 1):
39
+ if port in exclude_ports:
40
+ continue
41
+
42
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
43
+ sock.settimeout(1)
44
+ result = sock.connect_ex(('127.0.0.1', port))
45
+ sock.close()
46
+
47
+ if result != 0:
48
+ return port
49
+
50
+ raise RuntimeError(f"No free port found in range {start_port}-{end_port}")
51
+
52
+
53
+ def get_public_ip() -> Optional[str]:
54
+ services = [
55
+ 'https://ifconfig.me',
56
+ 'https://api.ipify.org',
57
+ 'https://icanhazip.com',
58
+ 'https://checkip.amazonaws.com'
59
+ ]
60
+
61
+ for service in services:
62
+ try:
63
+ with urllib.request.urlopen(service, timeout=5) as response:
64
+ ip = response.read().decode('utf-8').strip()
65
+ if ip:
66
+ return ip
67
+ except Exception:
68
+ continue
69
+
70
+ return None
71
+
72
+
73
+ def get_local_ip() -> str:
74
+ try:
75
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
76
+ s.connect(('8.8.8.8', 80))
77
+ ip = s.getsockname()[0]
78
+ s.close()
79
+ return ip
80
+ except Exception:
81
+ return '127.0.0.1'
82
+
83
+
84
+ def check_port_available(port: int, host: str = '0.0.0.0') -> bool:
85
+ try:
86
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
87
+ sock.settimeout(1)
88
+ result = sock.connect_ex((host, port))
89
+ sock.close()
90
+ return result != 0
91
+ except Exception:
92
+ return False
93
+
94
+
95
+ def get_common_ports() -> List[int]:
96
+ return [443, 8443, 8080, 5001, 5047, 5050, 8081, 9443, 2053, 2096, 2087, 2083]
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-mtproxy-lib
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Production-ready MTProto proxy server library with multi-secret support, statistics, and daemon mode
5
5
  Home-page: https://github.com/twosleepynights0x1/py-mtproxy-lib
6
- Author: Your Name
6
+ Author: AneK
7
7
  Author-email: tashova28@gmail.com
8
8
  Project-URL: Bug Reports, https://github.com/twosleepynights0x1/py-mtproxy-lib
9
9
  Keywords: mtproto telegram proxy vpn daemon docker
@@ -7,6 +7,7 @@ mtproxy/daemon.py
7
7
  mtproxy/logger.py
8
8
  mtproxy/server.py
9
9
  mtproxy/stats.py
10
+ mtproxy/utils.py
10
11
  py_mtproxy_lib.egg-info/PKG-INFO
11
12
  py_mtproxy_lib.egg-info/SOURCES.txt
12
13
  py_mtproxy_lib.egg-info/dependency_links.txt
@@ -5,8 +5,8 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="py-mtproxy-lib",
8
- version="0.0.2",
9
- author="Your Name",
8
+ version="0.0.4",
9
+ author="AneK",
10
10
  author_email="tashova28@gmail.com",
11
11
  description="Production-ready MTProto proxy server library with multi-secret support, statistics, and daemon mode",
12
12
  long_description=long_description,
File without changes
File without changes