少女祈祷中...

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 两种方式进行部署。下方是部署的代码。
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 worker 部署

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

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

    新建 Worker 服务

    点击「部署」

    点击「部署」

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

    点击右上角「编辑代码」

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

  4. 点击「保存并部署」

    点击「保存并部署」

  5. 将 「兼容日期」 改为 26 年之前的任意一个日期,26 年 4 月 群友反馈 部署使用不了,就是这个原因,我在这里补充出来。
    将 「兼容日期」 改为 26年之前的任意一个日期

    将 「兼容日期」 改为 26 年之前的任意一个日期【如 2026-1-15】

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

  • 注意:虽然该域名在浏览器中直接访问可能无法打开(被墙),但不要担心,这正是 ECH 技术的优点之一。

  • 地址格式

    :请在域名后加上端口

    1
    :443

    • 示例:crimson-art-0bbc.zrfme.workers.dev:443

获取服务地址

获取服务地址

二、 客户端工具下载

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

群文件指引

群文件指引

三、 Android 手机版使用教程

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

  1. 服务器地址
    填写第一步中获取的 Workers 地址,格式必须为 域名:443
    • 示例crimson-art-0bbc.zrfme.workers.dev:443
  2. 优选 IP (核心步骤)
    • 情况 A(使用 workers.dev 默认域名):必须填写 Cloudflare 的优选 IP。因为默认域名 DNS 被污染,我们需要强制指定一个干净的 IP 进行握手。优选获取:CloudFlare 优选方案汇总 持续更新中
    • 情况 B(使用自定义域名):如果您的 Worker 绑定了自定义域名且该域名未被墙,不写优选 IP 也可直连,看自己使用情况而定。
    • 我图中所用 优选 IP 不保证能长时间使用。
  3. 身份令牌 (可选步骤)
    • 既安卓软件中的 身份令牌 ,PC 软件中的 TOKEN ,密码内容由你设置,在代码中修改此处设置const token = 'www.202520.xyz';,如设置需在手机端或电脑端进行填写。
  4. 启动代理
    根据需求选择「全局模式」或「分应用代理」,点击底部按钮启动即可。

安卓版配置图解

安卓版配置图解

四、 PC 电脑版配置教程

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

  1. 解压群文件内的 PC 版压缩包,双击运行 ech-win-gui.exe
    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 IP104.16.x.x 或优选域名,优选获取:CloudFlare 优选方案汇总 持续更新中
    ECH 域名固定值,勿改cloudflare-ech.com
    DOH 服务器必填dns.alidns.com/dns-query
    TOKEN 配置可选 ,既安卓软件中的 身份令牌 ,PC 软件中的 TOKEN ,密码内容由你设置在代码中修改此处设置const token = 'blog.zrf.me';
  3. 全部填写完毕后,我们打开 v2rayN 软件 或 NekoBox 这类的可以自由编辑节点的软件。我以 v2rayN 为例。点击左上角「配置项」→「添加 SOCKS」
    「添加 SOCKS」

    「添加 SOCKS」

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

    填写 SOCKS 配置项

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

    连接成功

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

致谢与来源说明

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

工具下载:如需获取工具或交流技术,可加入电报社群:点击加入电报社群 (@lisexyyy)(群文件含 PC / 安卓客户端)

说些什么吧!

valine