mirror of
https://github.com/Flowseal/tg-ws-proxy.git
synced 2026-10-21 02:58:45 +00:00
Пул CF worker: закрытие при остановке, OPTI, сборка, доки (#1411)
This commit is contained in:
1 parent
bf07b42f88
commit
c317406b52
11 files changed
+78
-20
No files matched your search
@@ -55,11 +55,12 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
|
|||||||
| `--no-secure` | `false` | Use 80 port for CF-proxy and CF-worker connections |
|
| `--no-secure` | `false` | Use 80 port for CF-proxy and CF-worker connections |
|
||||||
| `--fake-tls-domain` | | Enable Fake TLS masquerading (ee-secret) with specified SNI domain |
|
| `--fake-tls-domain` | | Enable Fake TLS masquerading (ee-secret) with specified SNI domain |
|
||||||
| `--proxy-protocol` | disabled | Accept HAProxy PROXY protocol v1 (for use behind nginx/haproxy with `proxy_protocol on`) |
|
| `--proxy-protocol` | disabled | Accept HAProxy PROXY protocol v1 (for use behind nginx/haproxy with `proxy_protocol on`) |
|
||||||
|
| `--force-test-dc` | `false` | Route all traffic to Telegram test DCs [Learn more](./TestDc.md) |
|
||||||
| `--buf-kb` | `256` | Buffer size in KB |
|
| `--buf-kb` | `256` | Buffer size in KB |
|
||||||
| `--pool-size` | `4` | Number of ready WS connections per DC. `0` disables the direct DC→IP WS route |
|
| `--pool-size` | `4` | Number of ready WS connections per DC. `0` disables the direct DC→IP WS route |
|
||||||
| `--log-file` | disabled | Path to file for saving logs |
|
| `--log-file` | disabled | Path to file for saving logs |
|
||||||
| `--log-max-mb` | `5` | Maximum log file size in MB (afterwards overwrites) |
|
| `--log-max-mb` | `5` | Maximum log file size in MB (afterwards overwrites) |
|
||||||
| `--log-backups` | `0` | Number of log backups after overwrite |
|
| `--log-backups` | `1` | Number of rotated log files to keep (minimum 1) |
|
||||||
| `-v`, `--verbose` | disabled | Verbose logging (DEBUG) |
|
| `-v`, `--verbose` | disabled | Verbose logging (DEBUG) |
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|||||||
@@ -18,13 +18,18 @@ The tray application stores data in:
|
|||||||
"verbose": false,
|
"verbose": false,
|
||||||
"buf_kb": 256,
|
"buf_kb": 256,
|
||||||
"pool_size": 4,
|
"pool_size": 4,
|
||||||
"log_max_mb": 5.0,
|
"log_max_mb": 5,
|
||||||
"check_updates": true,
|
"check_updates": true,
|
||||||
"cfproxy": true,
|
"cfproxy": true,
|
||||||
"cfproxy_user_domain": "",
|
"h2": true,
|
||||||
"cfproxy_worker_domain": "",
|
"cfproxy_user_domain_enabled": false,
|
||||||
|
"cfproxy_user_domain": [],
|
||||||
|
"cfproxy_worker_enabled": false,
|
||||||
|
"cfproxy_worker_domain": [],
|
||||||
"force_test_dc": false,
|
"force_test_dc": false,
|
||||||
"appearance": "auto"
|
"no_secure": false,
|
||||||
|
"appearance": "auto",
|
||||||
|
"language": "en"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,12 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
|
|||||||
| `--no-secure` | `false` | Принудительно использует 80 порт для CF-proxy и CF-worker |
|
| `--no-secure` | `false` | Принудительно использует 80 порт для CF-proxy и CF-worker |
|
||||||
| `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом |
|
| `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом |
|
||||||
| `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) |
|
| `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) |
|
||||||
|
| `--force-test-dc` | `false` | Направлять весь трафик на тестовые DC Telegram [Подробнее](./TestDc.md) |
|
||||||
| `--buf-kb` | `256` | Размер буфера в КБ |
|
| `--buf-kb` | `256` | Размер буфера в КБ |
|
||||||
| `--pool-size` | `4` | Количество заготовленных WS-соединений на каждый DC. `0` отключает прямой WS-маршрут DC→IP |
|
| `--pool-size` | `4` | Количество заготовленных WS-соединений на каждый DC. `0` отключает прямой WS-маршрут DC→IP |
|
||||||
| `--log-file` | выкл. | Путь к файлу, в который будут сохраняться логи |
|
| `--log-file` | выкл. | Путь к файлу, в который будут сохраняться логи |
|
||||||
| `--log-max-mb` | `5` | Максимальный размер файла логов в МБ (после этого начинается перезапись) |
|
| `--log-max-mb` | `5` | Максимальный размер файла логов в МБ (после этого начинается перезапись) |
|
||||||
| `--log-backups` | `0` | Количество сохранений логов после перезаписи |
|
| `--log-backups` | `1` | Сколько старых файлов логов хранить при ротации (минимум 1) |
|
||||||
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) |
|
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) |
|
||||||
|
|
||||||
**Примеры:**
|
**Примеры:**
|
||||||
|
|||||||
@@ -18,13 +18,18 @@ Tray-приложение хранит данные в:
|
|||||||
"verbose": false,
|
"verbose": false,
|
||||||
"buf_kb": 256,
|
"buf_kb": 256,
|
||||||
"pool_size": 4,
|
"pool_size": 4,
|
||||||
"log_max_mb": 5.0,
|
"log_max_mb": 5,
|
||||||
"check_updates": true,
|
"check_updates": true,
|
||||||
"cfproxy": true,
|
"cfproxy": true,
|
||||||
"cfproxy_user_domain": "",
|
"h2": true,
|
||||||
"cfproxy_worker_domain": "",
|
"cfproxy_user_domain_enabled": false,
|
||||||
|
"cfproxy_user_domain": [],
|
||||||
|
"cfproxy_worker_enabled": false,
|
||||||
|
"cfproxy_worker_domain": [],
|
||||||
"force_test_dc": false,
|
"force_test_dc": false,
|
||||||
"appearance": "auto"
|
"no_secure": false,
|
||||||
|
"appearance": "auto",
|
||||||
|
"language": "ru"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ a = Analysis(
|
|||||||
[os.path.join(os.path.dirname(SPEC), os.pardir, 'linux.py')],
|
[os.path.join(os.path.dirname(SPEC), os.pardir, 'linux.py')],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=appindicator_binaries,
|
binaries=appindicator_binaries,
|
||||||
datas=[(ctk_path, 'customtkinter/'), (_i18n_path, 'ui/i18n')],
|
datas=[(os.path.join(ctk_path, 'assets'), 'customtkinter/assets'), (_i18n_path, 'ui/i18n')],
|
||||||
hiddenimports=[
|
hiddenimports=[
|
||||||
'httpx',
|
'httpx',
|
||||||
'h2',
|
'h2',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ a = Analysis(
|
|||||||
[os.path.join(os.path.dirname(SPEC), os.pardir, 'macos.py')],
|
[os.path.join(os.path.dirname(SPEC), os.pardir, 'macos.py')],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[(ctk_path, 'customtkinter/'), (_i18n_path, 'ui/i18n')] + certifi_datas,
|
datas=[(os.path.join(ctk_path, 'assets'), 'customtkinter/assets'), (_i18n_path, 'ui/i18n')] + certifi_datas,
|
||||||
hiddenimports=[
|
hiddenimports=[
|
||||||
'httpx',
|
'httpx',
|
||||||
'h2',
|
'h2',
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ a = Analysis(
|
|||||||
[os.path.join(os.path.dirname(SPEC), os.pardir, 'windows.py')],
|
[os.path.join(os.path.dirname(SPEC), os.pardir, 'windows.py')],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[(ctk_path, 'customtkinter/'), (_i18n_path, 'ui/i18n')] + certifi_datas,
|
datas=[(os.path.join(ctk_path, 'assets'), 'customtkinter/assets'), (_i18n_path, 'ui/i18n')] + certifi_datas,
|
||||||
hiddenimports=[
|
hiddenimports=[
|
||||||
'httpx',
|
'httpx',
|
||||||
'h2',
|
'h2',
|
||||||
|
|||||||
+23
-5
@@ -5,7 +5,7 @@ import time
|
|||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from typing import Dict, List, Optional, Tuple, Set
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from .raw_websocket import RawWebSocket, WsHandshakeError
|
from .raw_websocket import RawWebSocket, WsHandshakeError
|
||||||
from .stats import stats
|
from .stats import stats
|
||||||
@@ -237,7 +237,7 @@ class _CfWorkerPool:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._idle: Dict[int, deque] = {}
|
self._idle: Dict[int, deque] = {}
|
||||||
self._refilling: Set[int] = set()
|
self._refilling: Dict[int, asyncio.Task] = {}
|
||||||
self._exhausted_until: Dict[str, float] = {}
|
self._exhausted_until: Dict[str, float] = {}
|
||||||
|
|
||||||
async def get(self, dc: int, fallback_dst: str,
|
async def get(self, dc: int, fallback_dst: str,
|
||||||
@@ -269,8 +269,7 @@ class _CfWorkerPool:
|
|||||||
def _schedule_refill(self, dc, fallback_dst, worker_domains):
|
def _schedule_refill(self, dc, fallback_dst, worker_domains):
|
||||||
if dc in self._refilling:
|
if dc in self._refilling:
|
||||||
return
|
return
|
||||||
self._refilling.add(dc)
|
self._refilling[dc] = asyncio.create_task(self._refill(
|
||||||
asyncio.create_task(self._refill(
|
|
||||||
dc, fallback_dst, list(worker_domains)))
|
dc, fallback_dst, list(worker_domains)))
|
||||||
|
|
||||||
async def _refill(self, dc, fallback_dst, worker_domains):
|
async def _refill(self, dc, fallback_dst, worker_domains):
|
||||||
@@ -287,11 +286,15 @@ class _CfWorkerPool:
|
|||||||
if connected is None:
|
if connected is None:
|
||||||
break
|
break
|
||||||
ws, worker_domain = connected
|
ws, worker_domain = connected
|
||||||
|
if self._refilling.get(dc) is not asyncio.current_task():
|
||||||
|
await self._quiet_close(ws)
|
||||||
|
return
|
||||||
bucket.append((ws, time.monotonic(), worker_domain))
|
bucket.append((ws, time.monotonic(), worker_domain))
|
||||||
log.debug("CF worker pool refilled DC%d: %d ready",
|
log.debug("CF worker pool refilled DC%d: %d ready",
|
||||||
dc, len(bucket))
|
dc, len(bucket))
|
||||||
finally:
|
finally:
|
||||||
self._refilling.discard(dc)
|
if self._refilling.get(dc) is asyncio.current_task():
|
||||||
|
self._refilling.pop(dc, None)
|
||||||
|
|
||||||
async def _connect_one(self, worker_domains, fallback_dst, dc):
|
async def _connect_one(self, worker_domains, fallback_dst, dc):
|
||||||
query = urlencode({
|
query = urlencode({
|
||||||
@@ -359,10 +362,25 @@ class _CfWorkerPool:
|
|||||||
log.info("CF worker pool warmup started for %d DC(s)", len(cf_fallbacks))
|
log.info("CF worker pool warmup started for %d DC(s)", len(cf_fallbacks))
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
for task in self._refilling.values():
|
||||||
|
if not task.done() and task.get_loop() is loop:
|
||||||
|
task.cancel()
|
||||||
|
for bucket in self._idle.values():
|
||||||
|
for ws, _, _ in bucket:
|
||||||
|
try:
|
||||||
|
ws.writer.close()
|
||||||
|
except Exception as exc:
|
||||||
|
log.debug("CF worker pool close failed: %r", exc)
|
||||||
self._idle.clear()
|
self._idle.clear()
|
||||||
self._refilling.clear()
|
self._refilling.clear()
|
||||||
self._exhausted_until.clear()
|
self._exhausted_until.clear()
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
tasks = list(self._refilling.values())
|
||||||
|
self.reset()
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
|
||||||
ws_pool = _WsPool()
|
ws_pool = _WsPool()
|
||||||
cf_worker_pool = _CfWorkerPool()
|
cf_worker_pool = _CfWorkerPool()
|
||||||
@@ -536,6 +536,7 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
|
|||||||
await _quiet_cancel(task)
|
await _quiet_cancel(task)
|
||||||
await server.wait_closed()
|
await server.wait_closed()
|
||||||
await ws_pool.close()
|
await ws_pool.close()
|
||||||
|
await cf_worker_pool.close()
|
||||||
if cf_h2_pool is not None:
|
if cf_h2_pool is not None:
|
||||||
await cf_h2_pool.close()
|
await cf_h2_pool.close()
|
||||||
cf_h2_pool = None
|
cf_h2_pool = None
|
||||||
|
|||||||
+2
-1
@@ -33,7 +33,8 @@ PROTO_PADDED_INTERMEDIATE_INT = 0xDDDDDDDD
|
|||||||
RESERVED_FIRST_BYTES = {0xEF}
|
RESERVED_FIRST_BYTES = {0xEF}
|
||||||
RESERVED_STARTS = {b'\x48\x45\x41\x44', b'\x50\x4F\x53\x54',
|
RESERVED_STARTS = {b'\x48\x45\x41\x44', b'\x50\x4F\x53\x54',
|
||||||
b'\x47\x45\x54\x20', b'\xee\xee\xee\xee',
|
b'\x47\x45\x54\x20', b'\xee\xee\xee\xee',
|
||||||
b'\xdd\xdd\xdd\xdd', b'\x16\x03\x01\x02'}
|
b'\xdd\xdd\xdd\xdd', b'\x16\x03\x01\x02',
|
||||||
|
b'\x4F\x50\x54\x49'}
|
||||||
RESERVED_CONTINUE = b'\x00\x00\x00\x00'
|
RESERVED_CONTINUE = b'\x00\x00\x00\x00'
|
||||||
|
|
||||||
_GITHUB_IPS: Dict[str, str] = {
|
_GITHUB_IPS: Dict[str, str] = {
|
||||||
|
|||||||
+27
-1
@@ -7,7 +7,7 @@ from types import SimpleNamespace
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from proxy.config import proxy_config
|
from proxy.config import proxy_config
|
||||||
from proxy.pool import _WsPool
|
from proxy.pool import _CfWorkerPool, _WsPool
|
||||||
from proxy.raw_websocket import RawWebSocket, WsHandshakeError
|
from proxy.raw_websocket import RawWebSocket, WsHandshakeError
|
||||||
from proxy.utils import ws_domains
|
from proxy.utils import ws_domains
|
||||||
|
|
||||||
@@ -241,5 +241,31 @@ class WsPoolTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
idle.writer.close.assert_called_once()
|
idle.writer.close.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class CfWorkerPoolTest(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def test_close_cancels_refill_and_closes_unclaimed_connections(self):
|
||||||
|
pool = _CfWorkerPool()
|
||||||
|
ready = _open_ws()
|
||||||
|
idle = _open_ws()
|
||||||
|
started = asyncio.Event()
|
||||||
|
pool._idle[2] = deque([(idle, time.monotonic(), 'a.example')])
|
||||||
|
|
||||||
|
async def connect(*args):
|
||||||
|
started.set()
|
||||||
|
try:
|
||||||
|
await asyncio.Future()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return ready, 'b.example'
|
||||||
|
|
||||||
|
with mock.patch.object(proxy_config, 'pool_size', 1), \
|
||||||
|
mock.patch.object(pool, '_connect_one', side_effect=connect):
|
||||||
|
pool._schedule_refill(4, '192.0.2.4', ['b.example'])
|
||||||
|
await asyncio.wait_for(started.wait(), 1)
|
||||||
|
await pool.close()
|
||||||
|
self.assertFalse(pool._idle)
|
||||||
|
self.assertFalse(pool._refilling)
|
||||||
|
ready.writer.close.assert_called_once()
|
||||||
|
idle.writer.close.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user