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 (параметр можно указывать несколько раз) | | `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (параметр можно указывать несколько раз) |
| `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare](./CfProxy.md) | | `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare](./CfProxy.md) |
| `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudflare. [Подробнее](./CfProxy.md) | | `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudflare. [Подробнее](./CfProxy.md) |
| `--cfproxy-worker-domain` | | Домен Cloudflare Worker [Подробнее](./CfWorker.md) |
| `--cfproxy-priority` | `true` | Пробовать проксировать через Cloudflare перед прямым TCP подключением | | `--cfproxy-priority` | `true` | Пробовать проксировать через Cloudflare перед прямым TCP подключением |
| `--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`) |

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": true,
"cfproxy_priority": true, "cfproxy_priority": true,
"cfproxy_user_domain": "", "cfproxy_user_domain": "",
"cfproxy_worker_domain": "",
"appearance": "auto" "appearance": "auto"
} }
``` ```

View File

@@ -4,6 +4,7 @@ import struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from typing import Dict, List, Optional from typing import Dict, List, Optional
from urllib.parse import urlencode
from .utils import * from .utils import *
from .stats import stats from .stats import stats
@@ -132,14 +133,24 @@ async def do_fallback(reader, writer, relay_init, label,
fallback_dst = DC_DEFAULT_IPS.get(dc) fallback_dst = DC_DEFAULT_IPS.get(dc)
use_cf = proxy_config.fallback_cfproxy use_cf = proxy_config.fallback_cfproxy
cf_first = proxy_config.fallback_cfproxy_priority cf_first = proxy_config.fallback_cfproxy_priority
worker_domain = proxy_config.cfproxy_worker_domain
methods: List[str] = ['tcp'] methods: List[str] = ['tcp']
if use_cf: if use_cf:
methods.insert(0 if cf_first else 1, 'cf') methods.insert(0 if cf_first else 1, 'cf')
if worker_domain:
methods.insert(0, 'cf_worker')
for method in methods: 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( ok = await _cfproxy_fallback(
reader, writer, relay_init, label, ctx, reader, writer, relay_init, label, ctx,
dc=dc, is_media=is_media, dc=dc, is_media=is_media,
@@ -157,6 +168,42 @@ async def do_fallback(reader, writer, relay_init, label,
return False 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, async def _cfproxy_fallback(reader, writer, relay_init, label,
ctx: CryptoCtx, ctx: CryptoCtx,
dc: int, is_media: bool, dc: int, is_media: bool,

View File

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

View File

@@ -78,7 +78,8 @@ class RawWebSocket:
self._closed = False self._closed = False
@staticmethod @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( reader, writer = await asyncio.wait_for(
asyncio.open_connection(host, 443, ssl=_ssl_ctx, asyncio.open_connection(host, 443, ssl=_ssl_ctx,
server_hostname=domain), server_hostname=domain),
@@ -89,7 +90,7 @@ class RawWebSocket:
ws_key = base64.b64encode(os.urandom(16)).decode() ws_key = base64.b64encode(os.urandom(16)).decode()
req = ( req = (
f'GET /apiws HTTP/1.1\r\n' f'GET {path} HTTP/1.1\r\n'
f'Host: {domain}\r\n' f'Host: {domain}\r\n'
f'Upgrade: websocket\r\n' f'Upgrade: websocket\r\n'
f'Connection: Upgrade\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' prio = 'CF first' if proxy_config.fallback_cfproxy_priority else 'TCP first'
user_domain = "user" if proxy_config.cfproxy_user_domain else "auto" user_domain = "user" if proxy_config.cfproxy_user_domain else "auto"
log.info(" CF proxy: enabled (%s | %s)", prio, user_domain) 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("=" * 60)
log.info(" Connect:") log.info(" Connect:")
if ftls: if ftls:
@@ -687,6 +690,10 @@ def main():
ap.add_argument('--cfproxy-domain', type=str, default='', ap.add_argument('--cfproxy-domain', type=str, default='',
metavar='DOMAIN', metavar='DOMAIN',
help='User defined Cloudflare-proxied domain for WS fallback') 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', ap.add_argument('--no-cfproxy', action='store_true',
help='Disable Cloudflare proxy fallback') help='Disable Cloudflare proxy fallback')
ap.add_argument('--cfproxy-priority', type=_parse_bool, default=True, 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 = not args.no_cfproxy
proxy_config.fallback_cfproxy_priority = args.cfproxy_priority proxy_config.fallback_cfproxy_priority = args.cfproxy_priority
proxy_config.cfproxy_user_domain = args.cfproxy_domain.strip() 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.fake_tls_domain = args.fake_tls_domain.strip()
proxy_config.proxy_protocol = args.proxy_protocol proxy_config.proxy_protocol = args.proxy_protocol

View File

@@ -20,6 +20,7 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
"cfproxy": True, "cfproxy": True,
"cfproxy_priority": True, "cfproxy_priority": True,
"cfproxy_user_domain": "", "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 = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"])
pc.fallback_cfproxy_priority = cfg.get("cfproxy_priority", DEFAULT_CONFIG["cfproxy_priority"]) 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_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 return True