免费不限流量!ECH-Workers 代理部署教程:Workers/Snippets 双方案 + PC / 安卓多端适配

在上一篇《Cloudflare Snippets 免费部署VLESS不限流量节点:MiSub用户进阶教程,实现订阅聚合与优选》中,不少朋友反馈没权限用 Snippets—— 这次直接上全员可用的 Workers 方案!

这是一个基于WebSocket的安全隧道代理项目,支持TCP转发、SOCKS5/HTTP代理,还通过ECH技术隐藏SNI,进一步提升安全性。

本文教你用 Cloudflare Workers/Snippets 部署 ECH-Workers,搭配专用客户端就能实现科学上网,就算 SNI 被墙也能稳定连接。下面简单介绍下核心用到的ECH协议——我也不太懂这个技术细节,据说能让网络连接更安全。

ECH协议核心作用简介

  • ECH协议(Encrypted Client Hello,加密客户端问候)是Cloudflare推出的加密技术,其核心作用是隐藏你的访问域名(SNI),具体原理与优势如下:
  • 在传统HTTPS连接中,SNI(服务器名称指示)字段以明文形式传输,这就导致中间网络(如运营商、网关等)能轻易识别你所访问的具体站点,存在隐私泄露和针对性封锁的风险。
  • 而ECH协议会将SNI信息加密后再发送,中间网络仅能检测到你与Cloudflare建立了连接,却无法解析出你实际访问的目标域名。这一特性大幅提升了网络访问的隐私安全性与抗封锁能力。
  • 简单来说:开启ECH后,别人只知道你连了Cloudflare,却不知道你具体连的是哪个站点~
  • 详细说明可查看 Cloudflare 官方文档:ECH协议
  • 另外可以通过 浏览体验安全检查 测试自身环境——目前我的测试结果里,除了“安全 SNI”未通过,其余项均正常。不过查了一圈,似乎需要开启浏览器的特定功能才能触发,但暂时没找到对应的设置方式。

一、 ECH-Workers 服务部署

  • ECH-Workers 部署方式,可使用 Cloudflare Snippets 与 Cloudflare worker 两种方式进行部署。下方是部署的代码。
  • 群内提供我已部署好的 Snippets 版 ECH 服务地址,可直接加群获取使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
const WS_READY_STATE_OPEN = 1;
const WS_READY_STATE_CLOSING = 2;
const CF_FALLBACK_IPS = ['[2a00:1098:2b::1:6815:5881]'];

// 复用 TextEncoder,避免重复创建
const encoder = new TextEncoder();

import { connect } from 'cloudflare:sockets';

export default {
async fetch(request, env, ctx) {
try {
const token = '';
const upgradeHeader = request.headers.get('Upgrade');

if (!upgradeHeader || upgradeHeader.toLowerCase() !== 'websocket') {
return new URL(request.url).pathname === '/'
? new Response('WebSocket Proxy Server', { status: 200 })
: new Response('Expected WebSocket', { status: 426 });
}

if (token && request.headers.get('Sec-WebSocket-Protocol') !== token) {
return new Response('Unauthorized', { status: 401 });
}

const [client, server] = Object.values(new WebSocketPair());
server.accept();

handleSession(server).catch(() => safeCloseWebSocket(server));

// 修复 spread 类型错误
const responseInit = {
status: 101,
webSocket: client
};

if (token) {
responseInit.headers = { 'Sec-WebSocket-Protocol': token };
}

return new Response(null, responseInit);

} catch (err) {
return new Response(err.toString(), { status: 500 });
}
},
};

async function handleSession(webSocket) {
let remoteSocket, remoteWriter, remoteReader;
let isClosed = false;

const cleanup = () => {
if (isClosed) return;
isClosed = true;

try { remoteWriter?.releaseLock(); } catch {}
try { remoteReader?.releaseLock(); } catch {}
try { remoteSocket?.close(); } catch {}

remoteWriter = remoteReader = remoteSocket = null;
safeCloseWebSocket(webSocket);
};

const pumpRemoteToWebSocket = async () => {
try {
while (!isClosed && remoteReader) {
const { done, value } = await remoteReader.read();

if (done) break;
if (webSocket.readyState !== WS_READY_STATE_OPEN) break;
if (value?.byteLength > 0) webSocket.send(value);
}
} catch {}

if (!isClosed) {
try { webSocket.send('CLOSE'); } catch {}
cleanup();
}
};

const parseAddress = (addr) => {
if (addr[0] === '[') {
const end = addr.indexOf(']');
return {
host: addr.substring(1, end),
port: parseInt(addr.substring(end + 2), 10)
};
}
const sep = addr.lastIndexOf(':');
return {
host: addr.substring(0, sep),
port: parseInt(addr.substring(sep + 1), 10)
};
};

const isCFError = (err) => {
const msg = err?.message?.toLowerCase() || '';
return msg.includes('proxy request') ||
msg.includes('cannot connect') ||
msg.includes('cloudflare');
};

const connectToRemote = async (targetAddr, firstFrameData) => {
const { host, port } = parseAddress(targetAddr);
const attempts = [null, ...CF_FALLBACK_IPS];

for (let i = 0; i < attempts.length; i++) {
try {
remoteSocket = connect({
hostname: attempts[i] || host,
port
});

if (remoteSocket.opened) await remoteSocket.opened;

remoteWriter = remoteSocket.writable.getWriter();
remoteReader = remoteSocket.readable.getReader();

// 发送首帧数据
if (firstFrameData) {
await remoteWriter.write(encoder.encode(firstFrameData));
}

webSocket.send('CONNECTED');
pumpRemoteToWebSocket();
return;

} catch (err) {
// 清理失败的连接
try { remoteWriter?.releaseLock(); } catch {}
try { remoteReader?.releaseLock(); } catch {}
try { remoteSocket?.close(); } catch {}
remoteWriter = remoteReader = remoteSocket = null;

// 如果不是 CF 错误或已是最后尝试,抛出错误
if (!isCFError(err) || i === attempts.length - 1) {
throw err;
}
}
}
};

webSocket.addEventListener('message', async (event) => {
if (isClosed) return;

try {
const data = event.data;

if (typeof data === 'string') {
if (data.startsWith('CONNECT:')) {
const sep = data.indexOf('|', 8);
await connectToRemote(
data.substring(8, sep),
data.substring(sep + 1)
);
}
else if (data.startsWith('DATA:')) {
if (remoteWriter) {
await remoteWriter.write(encoder.encode(data.substring(5)));
}
}
else if (data === 'CLOSE') {
cleanup();
}
}
else if (data instanceof ArrayBuffer && remoteWriter) {
await remoteWriter.write(new Uint8Array(data));
}
} catch (err) {
try { webSocket.send('ERROR:' + err.message); } catch {}
cleanup();
}
});

webSocket.addEventListener('close', cleanup);
webSocket.addEventListener('error', cleanup);
}

function safeCloseWebSocket(ws) {
try {
if (ws.readyState === WS_READY_STATE_OPEN ||
ws.readyState === WS_READY_STATE_CLOSING) {
ws.close(1000, 'Server closed');
}
} catch {}
}

Cloudflare Snippets 部署

  1. 若选择 Cloudflare Snippets 部署,可参考教程:Cloudflare Snippets 免费部署VLESS不限流量节点:MiSub用户进阶教程,实现订阅聚合与优选

  2. 无需重复执行教程内所有步骤,仅需跳过「第二步:设置 UUID (可选)」,其余步骤完全一致,部署代码直接使用本文上方的 ECH-Workers 代码即可。

  3. 部署完成后,需将设置好的主机名整理为「域名:443」格式(示例:ech.zrf.me:443),该地址即为后续需使用的服务地址。

Cloudflare worker 部署

此方案最为实用,即使 Cloudflare 默认分配的 xxx.workers.dev 域名在中国大陆被阻断,配合 ECH 客户端也能实现无感直连。

  1. 新建 Worker 服务,名称可自定义(本文使用默认名称),完成创建后点击「部署」;
    新建 Worker 服务
    点击「部署」

  2. 部署完成后,点击右上角「编辑代码」;
    点击右上角「编辑代码」

  3. 进入代码编辑界面,清空编辑器原有代码,粘贴本文上方的 ECH-Workers 代码,点击「保存并部署」;
    点击「保存并部署」

  4. 获取服务地址,返回 Worker 概览页面,复制 Cloudflare 分配的默认域名(格式如 xxx.workers.dev)。

  • 注意:虽然该域名在浏览器中直接访问可能无法打开(被墙),但不要担心,这正是 ECH 技术的优点之一。
  • 地址格式:请在域名后加上端口 :443
    • 示例:crimson-art-0bbc.zrfme.workers.dev:443

获取服务地址

二、 客户端工具下载

请加入下方 Telegram 社群获取专用客户端(群文件中包含 PC 版和 Android 版):

  • 下载地址点击加入电报社群 (@lsmoo)
  • 软件说明
    • PC 版 (Win) :由群内大佬 @CCF 基于 CF_NAT 开源项目修改制作。
    • Android 版 :使用原版 APK。
    • 注:目前暂无 iOS 版本。

群文件指引

三、 Android 手机版使用教程

下载安装后,请按照以下逻辑填写关键信息:

  1. 服务器地址
    填写第一步中获取的 Workers 地址,格式必须为 域名:443

    • 示例crimson-art-0bbc.zrfme.workers.dev:443
  2. 优选 IP (核心步骤)

    • 情况 A(使用 workers.dev 默认域名):必须填写 Cloudflare 的优选 IP。因为默认域名DNS被污染,我们需要强制指定一个干净的 IP 进行握手。
    • 情况 B(使用自定义域名):如果您的 Worker 绑定了自定义域名且该域名未被墙。
    • 我图中所用 优选IP 不保证能长时间使用。
  3. 启动代理
    根据需求选择「全局模式」或「分应用代理」,点击底部按钮启动即可。

安卓版配置图解

四、 PC 电脑版配置教程

PC 端采用 “ECH GUI + v2rayN” 的组合模式。

  1. 解压群文件内的 PC 版压缩包,双击运行 ech-win-gui.exe
    ech-win-gui.exe

  2. 请按以下配置项要求,准确填写相关内容(图示参考见对应位置)
    配置项

    必填配置项 填写说明 示例值
    服务地址 你的 Workers 域名 + 端口 Workers部署 xxx.workers.dev:443 或 Snippets部署 ech.zrf.me:443
    监听地址 本地转发端口(建议默认) 127.0.0.1:30000
    优选IP/域名 必填,筛选后的 优选CF IP 104.16.x.x 或优选域名
    ECH域名 固定值,勿改 cloudflare-ech.com
    DOH服务器 必填 dns.alidns.com/dns-query
  3. 全部填写完毕后,我们打开 v2rayN 软件 或 NekoBox 这类的可以自由编辑节点的软件。我以 v2rayN 为例。点击左上角「配置项」→「添加 SOCKS」
    「添加 SOCKS」

  4. 填写 SOCKS 配置项:(图示参考见对应位置)
    填写 SOCKS 配置项

    必填配置项 内容
    名称 (随意填写)
    地址 127.0.0.1
    端口 30000
  5. 点击「确定」完成创建,随后点击「启动代理」,右键选择新建的节点设为「活动」,测试延迟显示正常后,开启「自动配置系统代理」即可使用。
    连接成功

  6. 好了,完结撒花。我只是简单写个流程图,大佬勿喷。

致谢与来源说明

特别感谢群内大佬 @CCF 基于 CF_NAT 开源项目,定制修改了适配本教程的 PC 端专用客户端,让部署后的使用更便捷;同时感谢 CF_NAT 频道提供的开源核心技术支持。
本文相关工具、技术方案均来源于:

工具下载:如需获取工具或交流技术,可加入电报社群:点击加入电报社群 (@lsmoo)(群文件含 PC/安卓客户端)
额外福利:群内提供我已部署好的 Snippets 版 ECH 服务地址,可直接加群获取使用;