This commit is contained in:
borisovmsw 2026-03-20 03:10:18 +03:00
parent 99c5340f09
commit 02dad8343e
3 changed files with 97 additions and 20 deletions

View File

@ -22,6 +22,7 @@ FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PATH=/opt/venv/bin:$PATH \ PATH=/opt/venv/bin:$PATH \
PYTHONPATH="/opt/venv/lib/python3.12/site-packages" \
TG_WS_PROXY_HOST=0.0.0.0 \ TG_WS_PROXY_HOST=0.0.0.0 \
TG_WS_PROXY_PORT=1080 \ TG_WS_PROXY_PORT=1080 \
TG_WS_PROXY_DC_IPS="2:149.154.167.220 4:149.154.167.220" TG_WS_PROXY_DC_IPS="2:149.154.167.220 4:149.154.167.220"

View File

@ -125,3 +125,79 @@ pyinstaller packaging/windows.spec
## Лицензия ## Лицензия
[MIT License](LICENSE) [MIT License](LICENSE)
## FORK
### Linux/NAS
```bash
sudo apt update && sudo apt install git
git clone https://github.com/borisovmsw/tg-ws-proxy.git
cd tg-ws-proxy
docker build -t tg-proxy .
docker run -d --name tg-proxy -p 1080:1080 tg-proxy:latest -u userx -P 123456
```
### Подключаемся и проверяем
#### Локально
```
tg://socks/?server=127.0.0.1&port=1080&user=userx&pass=123456
```
#### Удаленно
```
tg://socks/?server=192.168.1.139&port=1080&user=userx&pass=123456
```
### Openwrt ARM64
#### Настраиваем компьютер на компиляцию под ARM64 процессор роутера
```bash
docker run --privileged --rm tonistiigi/binfmt --install all\n
docker buildx create --name mybuilder --use
docker buildx inspect --bootstrap
```
#### Собираем образ для роутера
```bash
docker buildx build --platform linux/arm64 -t tg-proxy-flint:latest --load .\n
```
#### Сохраняем образ в файл
```bash
docker save tg-proxy-flint:latest -o tg-proxy-flint.tar
```
#### Закидываем образ на роутер
```bash
scp -O tg-proxy-flint.tar root@192.168.1.1:/tmp/
```
#### Подключаемся к роутеру
```bash
ssh root@192.168.1.1
```
#### Загружаем образ и удалям его
```bash
docker load -i /tmp/tg-proxy-flint.tar && rm /tmp/tg-proxy-flint.tar
```
#### Настройки роутера
```
Заходим в настройки Firewall - Traffic Rules
Создаем Rule с именем Docker,
Source zone - docker,
Destination zone - WAN
на закладке Advanced Settings оставляем только ip4
```
#### Далее самое главное!!! Включаем WAN для Docker, информации крайне мало про эту срочку, времени на ее поиск ушло не мало
```bash
sed -i "s/^\([[:space:]]*\)list blocked_interfaces 'wan'/#\1&/" /etc/config/dockerd
```
#### Перезагружаем роутер или же выполняем команду
```bash
/etc/init.d/dockerd restart
```
#### Запускаем контейнер только на внутреннем IP
```bash
docker run -d --name tg-proxy -p 192.168.1.1:1080:1080 tg-proxy-flint:latest -u userx -P 123456
```
#### Смотрим логи, практически все INFO на каждое соединение заменил на DEBUG, чтобы не тратить ресурс постоянной памяти
```bash
docker logs tg-proxy
```
#### Тестируем
```bash
tg://socks/?server=192.168.1.1&port=1080&user=userx&pass=123456
```

View File

@ -670,7 +670,7 @@ async def _bridge_ws(reader, writer, ws: RawWebSocket, label,
except BaseException: except BaseException:
pass pass
elapsed = asyncio.get_event_loop().time() - start_time elapsed = asyncio.get_event_loop().time() - start_time
log.info("[%s] %s (%s) WS session closed: " log.debug("[%s] %s (%s) WS session closed: "
"^%s (%d pkts) v%s (%d pkts) in %.1fs", "^%s (%d pkts) v%s (%d pkts) in %.1fs",
label, dc_tag, dst_tag, label, dc_tag, dst_tag,
_human_bytes(up_bytes), up_packets, _human_bytes(up_bytes), up_packets,
@ -765,7 +765,7 @@ async def _tcp_fallback(reader, writer, dst, port, init, label,
rr, rw = await asyncio.wait_for( rr, rw = await asyncio.wait_for(
asyncio.open_connection(dst, port), timeout=10) asyncio.open_connection(dst, port), timeout=10)
except Exception as exc: except Exception as exc:
log.warning("[%s] TCP fallback connect to %s:%d failed: %s", log.debug("[%s] TCP fallback connect to %s:%d failed: %s",
label, dst, port, exc) label, dst, port, exc)
return False return False
@ -823,7 +823,7 @@ async def _handle_client(reader, writer):
client_password = (await reader.readexactly(plen)).decode('utf-8', errors='ignore') client_password = (await reader.readexactly(plen)).decode('utf-8', errors='ignore')
if client_username != _auth_user or client_password != _auth_password: if client_username != _auth_user or client_password != _auth_password:
log.warning("[%s] auth failed with creds: %s/%s", label, client_username, client_password) log.debug("[%s] auth failed with creds: %s/%s", label, client_username, client_password)
writer.write(b'\x01\x01') writer.write(b'\x01\x01')
await writer.drain() await writer.drain()
writer.close() writer.close()
@ -866,7 +866,7 @@ async def _handle_client(reader, writer):
port = struct.unpack('!H', await reader.readexactly(2))[0] port = struct.unpack('!H', await reader.readexactly(2))[0]
if ':' in dst: if ':' in dst:
log.error( log.debug(
"[%s] IPv6 address detected: %s:%d" "[%s] IPv6 address detected: %s:%d"
"IPv6 addresses are not supported; " "IPv6 addresses are not supported; "
"disable IPv6 to continue using the proxy.", "disable IPv6 to continue using the proxy.",
@ -884,7 +884,7 @@ async def _handle_client(reader, writer):
rr, rw = await asyncio.wait_for( rr, rw = await asyncio.wait_for(
asyncio.open_connection(dst, port), timeout=10) asyncio.open_connection(dst, port), timeout=10)
except Exception as exc: except Exception as exc:
log.warning("[%s] passthrough failed to %s: %s: %s", label, dst, type(exc).__name__, str(exc) or "(no message)") log.debug("[%s] passthrough failed to %s: %s: %s", label, dst, type(exc).__name__, str(exc) or "(no message)")
writer.write(_socks5_reply(0x05)) writer.write(_socks5_reply(0x05))
await writer.drain() await writer.drain()
writer.close() writer.close()
@ -937,7 +937,7 @@ async def _handle_client(reader, writer):
init_patched = True init_patched = True
if dc is None or dc not in _dc_opt: if dc is None or dc not in _dc_opt:
log.warning("[%s] unknown DC%s for %s:%d -> TCP passthrough", log.debug("[%s] unknown DC%s for %s:%d -> TCP passthrough",
label, dc, dst, port) label, dc, dst, port)
await _tcp_fallback(reader, writer, dst, port, init, label) await _tcp_fallback(reader, writer, dst, port, init, label)
return return
@ -954,7 +954,7 @@ async def _handle_client(reader, writer):
ok = await _tcp_fallback(reader, writer, dst, port, init, ok = await _tcp_fallback(reader, writer, dst, port, init,
label, dc=dc, is_media=is_media) label, dc=dc, is_media=is_media)
if ok: if ok:
log.info("[%s] DC%d%s TCP fallback closed", log.debug("[%s] DC%d%s TCP fallback closed",
label, dc, media_tag) label, dc, media_tag)
return return
@ -967,7 +967,7 @@ async def _handle_client(reader, writer):
ok = await _tcp_fallback(reader, writer, dst, port, init, ok = await _tcp_fallback(reader, writer, dst, port, init,
label, dc=dc, is_media=is_media) label, dc=dc, is_media=is_media)
if ok: if ok:
log.info("[%s] DC%d%s TCP fallback closed", log.debug("[%s] DC%d%s TCP fallback closed",
label, dc, media_tag) label, dc, media_tag)
return return
@ -980,12 +980,12 @@ async def _handle_client(reader, writer):
ws = await _ws_pool.get(dc, is_media, target, domains) ws = await _ws_pool.get(dc, is_media, target, domains)
if ws: if ws:
log.info("[%s] DC%d%s (%s:%d) -> pool hit via %s", log.debug("[%s] DC%d%s (%s:%d) -> pool hit via %s",
label, dc, media_tag, dst, port, target) label, dc, media_tag, dst, port, target)
else: else:
for domain in domains: for domain in domains:
url = f'wss://{domain}/apiws' url = f'wss://{domain}/apiws'
log.info("[%s] DC%d%s (%s:%d) -> %s via %s", log.debug("[%s] DC%d%s (%s:%d) -> %s via %s",
label, dc, media_tag, dst, port, url, target) label, dc, media_tag, dst, port, url, target)
try: try:
ws = await RawWebSocket.connect(target, domain, ws = await RawWebSocket.connect(target, domain,
@ -996,14 +996,14 @@ async def _handle_client(reader, writer):
_stats.ws_errors += 1 _stats.ws_errors += 1
if exc.is_redirect: if exc.is_redirect:
ws_failed_redirect = True ws_failed_redirect = True
log.warning("[%s] DC%d%s got %d from %s -> %s", log.debug("[%s] DC%d%s got %d from %s -> %s",
label, dc, media_tag, label, dc, media_tag,
exc.status_code, domain, exc.status_code, domain,
exc.location or '?') exc.location or '?')
continue continue
else: else:
all_redirects = False all_redirects = False
log.warning("[%s] DC%d%s WS handshake: %s", log.debug("[%s] DC%d%s WS handshake: %s",
label, dc, media_tag, exc.status_line) label, dc, media_tag, exc.status_line)
except Exception as exc: except Exception as exc:
_stats.ws_errors += 1 _stats.ws_errors += 1
@ -1011,32 +1011,32 @@ async def _handle_client(reader, writer):
err_str = str(exc) err_str = str(exc)
if ('CERTIFICATE_VERIFY_FAILED' in err_str or if ('CERTIFICATE_VERIFY_FAILED' in err_str or
'Hostname mismatch' in err_str): 'Hostname mismatch' in err_str):
log.warning("[%s] DC%d%s SSL error: %s", log.debug("[%s] DC%d%s SSL error: %s",
label, dc, media_tag, exc) label, dc, media_tag, exc)
else: else:
log.warning("[%s] DC%d%s WS connect failed: %s", log.debug("[%s] DC%d%s WS connect failed: %s",
label, dc, media_tag, exc) label, dc, media_tag, exc)
# -- WS failed -> fallback -- # -- WS failed -> fallback --
if ws is None: if ws is None:
if ws_failed_redirect and all_redirects: if ws_failed_redirect and all_redirects:
_ws_blacklist.add(dc_key) _ws_blacklist.add(dc_key)
log.warning( log.debug(
"[%s] DC%d%s blacklisted for WS (all 302)", "[%s] DC%d%s blacklisted for WS (all 302)",
label, dc, media_tag) label, dc, media_tag)
elif ws_failed_redirect: elif ws_failed_redirect:
_dc_fail_until[dc_key] = now + _DC_FAIL_COOLDOWN _dc_fail_until[dc_key] = now + _DC_FAIL_COOLDOWN
else: else:
_dc_fail_until[dc_key] = now + _DC_FAIL_COOLDOWN _dc_fail_until[dc_key] = now + _DC_FAIL_COOLDOWN
log.info("[%s] DC%d%s WS cooldown for %ds", log.debug("[%s] DC%d%s WS cooldown for %ds",
label, dc, media_tag, int(_DC_FAIL_COOLDOWN)) label, dc, media_tag, int(_DC_FAIL_COOLDOWN))
log.info("[%s] DC%d%s -> TCP fallback to %s:%d", log.debug("[%s] DC%d%s -> TCP fallback to %s:%d",
label, dc, media_tag, dst, port) label, dc, media_tag, dst, port)
ok = await _tcp_fallback(reader, writer, dst, port, init, ok = await _tcp_fallback(reader, writer, dst, port, init,
label, dc=dc, is_media=is_media) label, dc=dc, is_media=is_media)
if ok: if ok:
log.info("[%s] DC%d%s TCP fallback closed", log.debug("[%s] DC%d%s TCP fallback closed",
label, dc, media_tag) label, dc, media_tag)
return return
@ -1060,7 +1060,7 @@ async def _handle_client(reader, writer):
splitter=splitter) splitter=splitter)
except asyncio.TimeoutError: except asyncio.TimeoutError:
log.warning("[%s] timeout during SOCKS5 handshake", label) log.debug("[%s] timeout during SOCKS5 handshake", label)
except asyncio.IncompleteReadError: except asyncio.IncompleteReadError:
log.debug("[%s] client disconnected", label) log.debug("[%s] client disconnected", label)
except asyncio.CancelledError: except asyncio.CancelledError:
@ -1116,7 +1116,7 @@ async def _run(port: int, dc_opt: Dict[int, Optional[str]],
bl = ', '.join( bl = ', '.join(
f'DC{d}{"m" if m else ""}' f'DC{d}{"m" if m else ""}'
for d, m in sorted(_ws_blacklist)) or 'none' for d, m in sorted(_ws_blacklist)) or 'none'
log.info("stats: %s | ws_bl: %s", _stats.summary(), bl) log.debug("stats: %s | ws_bl: %s", _stats.summary(), bl)
asyncio.create_task(log_stats()) asyncio.create_task(log_stats())