cloudflare worker implementation

This commit is contained in:
Flowseal
2026-05-16 10:44:57 +03:00
parent bff67b3ecf
commit 362c5a4893
9 changed files with 195 additions and 3 deletions

View File

@@ -48,6 +48,7 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
| `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (параметр можно указывать несколько раз) |
| `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare](./CfProxy.md) |
| `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudflare. [Подробнее](./CfProxy.md) |
| `--cfproxy-worker-domain` | | Домен Cloudflare Worker [Подробнее](./CfWorker.md) |
| `--cfproxy-priority` | `true` | Пробовать проксировать через Cloudflare перед прямым TCP подключением |
| `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом |
| `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) |

131
docs/CfWorker.md Normal file
View File

@@ -0,0 +1,131 @@
# Cloudflare Worker
Альтернативный (полностью бесплатный, не нужно покупать домен в отличии от [CfProxy](./CfProxy.md)) способ проксирования.
Прокси возвращает доступ к тому, что раньше не загружалось (реакции, некоторые стикеры). Если на аккаунте без Premium с данным способом все еще не загружаются фото/видео, оставьте в блоке `DC → IP` только `4:149.154.167.220`
##
1. **Добавьте в [zapret](https://github.com/Flowseal/zapret-discord-youtube/) или в любое другое ПО следующие домены:**
```
cloudflare.com
cloudflare.dev
workers.dev
```
2. Создайте аккаунт в [Cloudflare](https://dash.cloudflare.com/) (или войдите в существующий)
3. Слева в панели выберите `Compute``Workers & Pages`
4. Нажмите сверху справа кнопку **`Create application`** → `Start with Hello World!``Deploy`
5. Сверху справа нажмите кнопку **`Edit code`**, замените код слева на тот, что находится внизу страницы
* Если у вас не загружается код, то вы не выполнили первый пункт
6. Нажмите сверху справа кнопку **`Deploy`**
7. Скопируйте домен из поля справа и укажите его в настройках **Cloudflare Worker** (или через аргумент `--cfproxy-worker-domain`)
* Пример домена: `random-symbols-1234.username.workers.dev`
### Код Worker'а
```javascript
import { connect } from "cloudflare:sockets";
const DC_IPS = {
"1": "149.154.175.50",
"2": "149.154.167.51",
"3": "149.154.175.100",
"4": "149.154.167.91",
"5": "149.154.171.5",
"203": "91.105.192.100",
};
function isIpv4(value) {
return /^\d{1,3}(\.\d{1,3}){3}$/.test(value || "");
}
function pickDst(url) {
const byDst = url.searchParams.get("dst");
if (isIpv4(byDst)) {
return byDst;
}
const dc = url.searchParams.get("dc") || "2";
return DC_IPS[dc] || DC_IPS["2"];
}
function toBytes(data) {
if (data instanceof ArrayBuffer) {
return new Uint8Array(data);
}
if (typeof data === "string") {
return new TextEncoder().encode(data);
}
if (data && typeof data.arrayBuffer === "function") {
return data.arrayBuffer().then((ab) => new Uint8Array(ab));
}
return new Uint8Array();
}
export default {
async fetch(request) {
if ((request.headers.get("Upgrade") || "").toLowerCase() !== "websocket") {
return new Response("Expected websocket", { status: 426 });
}
const url = new URL(request.url);
if (url.pathname !== "/apiws") {
return new Response("Not found", { status: 404 });
}
const dst = pickDst(url);
const pair = new WebSocketPair();
const client = pair[0];
const server = pair[1];
server.accept();
const socket = connect({ hostname: dst, port: 443 });
const tcpReader = socket.readable.getReader();
const tcpWriter = socket.writable.getWriter();
server.addEventListener("message", async (event) => {
try {
await tcpWriter.write(await toBytes(event.data));
} catch {
try {
server.close(1011, "tcp write failed");
} catch {}
}
});
server.addEventListener("close", async () => {
try {
await tcpWriter.close();
} catch {}
try {
socket.close();
} catch {}
});
(async () => {
try {
while (true) {
const { value, done } = await tcpReader.read();
if (done) {
break;
}
if (value) {
server.send(value);
}
}
} catch {
} finally {
try {
server.close();
} catch {}
try {
tcpReader.releaseLock();
} catch {}
try {
socket.close();
} catch {}
}
})();
return new Response(null, { status: 101, webSocket: client });
},
};
```

View File

@@ -23,6 +23,7 @@ Tray-приложение хранит данные в:
"cfproxy": true,
"cfproxy_priority": true,
"cfproxy_user_domain": "",
"cfproxy_worker_domain": "",
"appearance": "auto"
}
```

View File

@@ -4,6 +4,7 @@ import struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from typing import Dict, List, Optional
from urllib.parse import urlencode
from .utils import *
from .stats import stats
@@ -132,14 +133,24 @@ async def do_fallback(reader, writer, relay_init, label,
fallback_dst = DC_DEFAULT_IPS.get(dc)
use_cf = proxy_config.fallback_cfproxy
cf_first = proxy_config.fallback_cfproxy_priority
worker_domain = proxy_config.cfproxy_worker_domain
methods: List[str] = ['tcp']
if use_cf:
methods.insert(0 if cf_first else 1, 'cf')
if worker_domain:
methods.insert(0, 'cf_worker')
for method in methods:
if method == 'cf':
if method == 'cf_worker' and fallback_dst:
ok = await _cfproxy_worker_fallback(
reader, writer, relay_init, label, ctx,
dc=dc, is_media=is_media, fallback_dst=fallback_dst,
splitter=splitter)
if ok:
return True
elif method == 'cf':
ok = await _cfproxy_fallback(
reader, writer, relay_init, label, ctx,
dc=dc, is_media=is_media,
@@ -157,6 +168,42 @@ async def do_fallback(reader, writer, relay_init, label,
return False
async def _cfproxy_worker_fallback(reader, writer, relay_init, label,
ctx: CryptoCtx,
dc: int, is_media: bool,
fallback_dst: str,
splitter=None):
media_tag = ' media' if is_media else ''
worker_domain = proxy_config.cfproxy_worker_domain
if not worker_domain:
return False
query = urlencode({
'dst': fallback_dst,
'dc': str(dc),
'media': '1' if is_media else '0',
})
path = f'/apiws?{query}'
log.info("[%s] DC%d%s -> trying CF worker for %s",
label, dc, media_tag, fallback_dst)
try:
ws = await RawWebSocket.connect(worker_domain, worker_domain,
timeout=10.0, path=path)
except Exception as exc:
log.warning("[%s] DC%d%s CF worker failed: %s",
label, dc, media_tag, repr(exc))
return False
stats.connections_cfproxy += 1
await ws.send(relay_init)
await bridge_ws_reencrypt(reader, writer, ws, label, ctx,
dc=dc, is_media=is_media,
splitter=splitter)
return True
async def _cfproxy_fallback(reader, writer, relay_init, label,
ctx: CryptoCtx,
dc: int, is_media: bool,

View File

@@ -60,6 +60,7 @@ class ProxyConfig:
fallback_cfproxy: bool = True
fallback_cfproxy_priority: bool = True
cfproxy_user_domain: str = ''
cfproxy_worker_domain: str = ''
fake_tls_domain: str = ''
proxy_protocol: bool = False

View File

@@ -78,7 +78,8 @@ class RawWebSocket:
self._closed = False
@staticmethod
async def connect(host: str, domain: str, timeout: float = 10.0) -> 'RawWebSocket':
async def connect(host: str, domain: str, timeout: float = 10.0,
path: str = '/apiws') -> 'RawWebSocket':
reader, writer = await asyncio.wait_for(
asyncio.open_connection(host, 443, ssl=_ssl_ctx,
server_hostname=domain),
@@ -89,7 +90,7 @@ class RawWebSocket:
ws_key = base64.b64encode(os.urandom(16)).decode()
req = (
f'GET /apiws HTTP/1.1\r\n'
f'GET {path} HTTP/1.1\r\n'
f'Host: {domain}\r\n'
f'Upgrade: websocket\r\n'
f'Connection: Upgrade\r\n'

View File

@@ -590,6 +590,9 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
prio = 'CF first' if proxy_config.fallback_cfproxy_priority else 'TCP first'
user_domain = "user" if proxy_config.cfproxy_user_domain else "auto"
log.info(" CF proxy: enabled (%s | %s)", prio, user_domain)
if proxy_config.cfproxy_worker_domain:
log.info(" CF worker: enabled (%s)",
proxy_config.cfproxy_worker_domain)
log.info("=" * 60)
log.info(" Connect:")
if ftls:
@@ -687,6 +690,10 @@ def main():
ap.add_argument('--cfproxy-domain', type=str, default='',
metavar='DOMAIN',
help='User defined Cloudflare-proxied domain for WS fallback')
ap.add_argument('--cfproxy-worker-domain', type=str, default='',
metavar='DOMAIN',
help='Cloudflare Worker domain for WS fallback '
'(tried before other fallback methods)')
ap.add_argument('--no-cfproxy', action='store_true',
help='Disable Cloudflare proxy fallback')
ap.add_argument('--cfproxy-priority', type=_parse_bool, default=True,
@@ -732,6 +739,7 @@ def main():
proxy_config.fallback_cfproxy = not args.no_cfproxy
proxy_config.fallback_cfproxy_priority = args.cfproxy_priority
proxy_config.cfproxy_user_domain = args.cfproxy_domain.strip()
proxy_config.cfproxy_worker_domain = args.cfproxy_worker_domain.strip()
proxy_config.fake_tls_domain = args.fake_tls_domain.strip()
proxy_config.proxy_protocol = args.proxy_protocol

View File

@@ -20,6 +20,7 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
"cfproxy": True,
"cfproxy_priority": True,
"cfproxy_user_domain": "",
"cfproxy_worker_domain": "",
}

View File

@@ -273,6 +273,7 @@ def apply_proxy_config(cfg: dict) -> bool:
pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"])
pc.fallback_cfproxy_priority = cfg.get("cfproxy_priority", DEFAULT_CONFIG["cfproxy_priority"])
pc.cfproxy_user_domain = cfg.get("cfproxy_user_domain", DEFAULT_CONFIG["cfproxy_user_domain"])
pc.cfproxy_worker_domain = cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG["cfproxy_worker_domain"])
return True