EasyTier Cloudflare Relay — 部署手册
在 Cloudflare Workers + Durable Objects 上部署 EasyTier 自建 WebSocket 节点(纯 P2P 优先模式)。 已用 easytier-core 2.6.4 官方客户端实测验证:握手、路由同步、双节点互见、数据中继全部通过。
1. 架构概览
EasyTier 客户端 (任意平台)
│ wss://et.example.com/ (标准 WebSocket + EasyTier 二进制协议)
▼
Cloudflare Edge (Worker 入口 src/index.js)
│ 按 URL/Header/Query 选择房间 → 转发到 Durable Object
▼
RelayRoom Durable Object (src/room.js)
│ 握手鉴权 → 路由同步(OSPF) → 包转发
▼
同一网络的其他客户端(经中继兜底;P2P 打洞成功后直连,不再经过本节点)
纯 P2P 模式:节点在路由协议中宣告 avoid_relay_data(OSPF 大代价边),
客户端之间优先直连;只有 P2P 打洞失败的流量才走中继兜底(可通过 RELAY_DATA=0 完全关闭数据面)。
2. 前置条件
| 项目 | 要求 |
|---|---|
| Cloudflare 账号 | 免费计划即可(SQLite 后端 Durable Objects 已对免费计划开放) |
| Node.js | ≥ 18(推荐 20+) |
| Wrangler | v4.x(npm i -g wrangler 或使用项目内 npx) |
| 自定义域名 | 可选;域名需托管在 Cloudflare(用 Workers 默认 *.workers.dev 域名则不需要) |
3. 快速部署(5 分钟)
# 1. 进入项目目录
cd easytier-cf-relay
# 2. 安装依赖
npm install
# 3. 登录 Cloudflare(浏览器授权)
npx wrangler login
# 4. 部署
npx wrangler deploy
# 5. 验证(把 <name> 替换为 wrangler 输出的 Worker 名)
curl https://easytier-cf-relay.<你的子域>.workers.dev/health
# 期望: {"ok":true}
客户端即可用 wss://easytier-cf-relay.<你的子域>.workers.dev/ 作为节点地址。
可选:启用 KV 审计(记录 + 黑名单)——创建 KV namespace 并在 wrangler.toml 启用
AUDIT_KV绑定(见 5.6 节)。不配置时功能自动降级为仅 DO storage 存储, 管理页功能不受影响。
本地验证(可选,无需账号):
npx wrangler dev --port 8787 # 另一个终端:完整协议测试(44 项) node test/e2e_test.mjs http://127.0.0.1:8787本地
wrangler dev会自动读取.dev.vars(已 gitignore,不随 deploy 上传), 其中预置了 E2E 测试所需的METRICS_PATH/ADMIN_PATH等 token。
4. 绑定自定义域名
方式 A:Custom Domain(推荐)
域名已托管在 Cloudflare 时,在 wrangler.toml 追加:
routes = [
{ pattern = "et.example.com", custom_domain = true }
]
重新 npx wrangler deploy。Cloudflare 会自动:
- 创建 DNS 记录
- 签发并轮换 TLS 证书(免费)
方式 B:Route + 手动 DNS
routes = [
{ pattern = "et.example.com/*", zone_name = "example.com" }
]
并在 Cloudflare DNS 面板为 et.example.com 添加任意 A/AAAA 记录(如 192.0.2.1,流量走 Worker 不回源)。
客户端 URL 格式
wss://et.example.com/
路径任意(/、/ws、/easytier 均可),与官方服务端行为一致——因为部分客户端
(含官方 GUI)会把用户配置的路径原样带到 WebSocket 升级请求里。
5. 配置参考
所有配置通过 wrangler.toml 的 [vars](或 wrangler secret)设置,改完重新 deploy 生效。
5.1 基础配置
| 变量 | 默认值 | 说明 |
|---|---|---|
SERVER_PEER_ID |
10000001 |
服务端虚拟 Peer ID。避免与客户端随机 ID 冲突(客户端 ID 为大随机数) |
SERVER_NETWORK_NAME |
public_server |
握手响应中宣告的网络名。与客户端网络名不同 ⇒ 官方客户端按"外部网络中继"处理(行为与官方公共服务器一致) |
SERVER_HOSTNAME |
easytier-cf-relay |
节点主机名(路由表中显示) |
SERVER_VERSION_STR |
easytier-cf-relay/1.4.0 |
节点版本串(easytier-cli peer 可见) |
5.2 P2P 与转发行为
| 变量 | 默认值 | 说明 |
|---|---|---|
AVOID_RELAY_DATA |
1 |
纯 P2P 模式:在 feature_flag 中宣告 avoid_relay_data,OSPF 给本节点边赋大代价,客户端优先直连 |
RELAY_DATA |
1 |
P2P 失败时的兜底:1 转发 Data/KCP 数据包;0 丢弃(严格纯 P2P,只保留控制面) |
5.3 容量与超时
| 变量 | 默认值 | 说明 |
|---|---|---|
MAX_PEERS_PER_ROOM |
64 |
单房间最大 peer 数(含所有网络分组) |
MAX_CONNS_PER_IP |
6 |
单 IP 并发连接上限(v1.4.1 事前限流):同一 IP 的开放连接数达到上限后,新升级请求返回 429 + Retry-After。DO 升级层权威检查;同 NAT 多节点共享出口 IP 时按需调大;0 = 关闭。前提:与边缘 IP 黑名单一样,依赖 Cloudflare 边缘注入的 CF-Connecting-IP——非 CF 前置的直连链路或本地 dev 下该头缺失时自动跳过(DO 层黑名单仍生效) |
MSG_RATE_LIMIT_PER_SEC |
50 |
单连接消息速率上限(v1.5 事前限流):每连接每秒最大消息数,超限先告警、持续超限断开连接(4008 rate limited)并计入总览「速率限流」计数。EasyTier 心跳 8s 1 个、路由同步爆发每秒数十个,默认 50 宽裕;0 = 关闭 |
MAX_PEERS_PER_GROUP |
32 |
每分组节点数上限(v1.5 事前限流):单个网络分组的在线节点数达到上限后,该网络的新节点握手被拒(4030 group full)并计入总览「分组限流」计数,其它网络不受影响;同 peerId 重连顶替不受限;0 = 不限 |
MAX_MESSAGE_BYTES |
131072 |
单条 WS 消息上限(128 KiB,远大于 EasyTier MTU 1380 与路由同步包) |
MAX_SYNC_ITEMS |
256 |
资源滥用防线:单帧路由同步条目数上限,超出整帧拒绝(4009);0 = 关闭。默认 = 房间上限 64 的 4× 余量 |
MAX_ROUTE_INFO_BYTES |
768 |
单条 RoutePeerInfo 原始字节上限,超出丢弃该条(防巨型条目类状态膨胀);0 = 关闭 |
MAX_ROUTES_PER_GROUP |
128 |
每分组路由条目总数上限(transit/幽灵总量闸门,兼作出站全量推送体积上界);0 = 关闭。与 MAX_ROUTE_INFO_BYTES 乘积须 < MAX_MESSAGE_BYTES(不自洽时启动日志 warn) |
MAX_DIRECT_PEERS_REPORT |
64 |
PeerCenter 单上报者直连表条目上限,超出截断;0 = 关闭 |
FULL_RESYNC_COOLDOWN_MS |
1000 |
全量重推最小间隔(会话重置 / GetGlobalPeerMap 全量响应共用,每连接);0 = 关闭 |
MAX_NETWORK_NAME_BYTES |
128 |
握手网络名长度上限(空名/超长名拒绝,close 4002);0 = 关闭 |
HANDSHAKE_TIMEOUT_MS |
15000 |
连接后未完成握手的清理超时(防幽灵连接第一道防线) |
PEER_IDLE_TIMEOUT_MS |
75000 |
空闲超时。必须 > 客户端 ping 最大间隔 32s(防幽灵节点第二道防线) |
SERVER_PING_IDLE_MS |
40000 |
服务端主动 ping 阈值,探测半开连接(第三道防线) |
ROUTE_INFO_UNREACHABLE_MS |
90000 |
路由条目老化(防线五):条目未刷新且节点不可达(无连接且 PeerCenter 无可达路径)超过该时长 → 删除。对齐官方 clear_expired_peer 语义 |
ROUTE_INFO_TTL_MS |
3660000 |
路由条目老化(防线五):条目未刷新超过该时长 → 无条件删除(61 分钟) |
GROUP_AUTO_DELETE_MS |
60000 |
空分组自动删除宽限(防线六):分组内在线节点归零后持续该时长仍为空 → 整组删除(含路由条目与摘要注册);0 = 关闭 |
SWEEP_INTERVAL_MS |
15000 |
alarm 清扫周期上限(老化与空分组删除也在此周期内执行)。有连接时按自适应排程取「该周期」与「最近 socket 事件时刻」的较小值 |
SWEEP_IDLE_INTERVAL_MS |
300000 |
空闲退避(v1.4.0):房间无连接且无脏数据时 alarm 退避到该间隔(空房间消耗从 5,760 请求/天降至 288);0 = 关闭 |
5.4 安全
| 变量 | 默认值 | 说明 |
|---|---|---|
STRICT_DIGEST |
1 |
1:同一网络名出现不同 network_secret 摘要 ⇒ 拒绝(防网络名抢占/嗅探);0:隔离为不同分组 |
TARPIT_UNKNOWN |
1 |
未知路径 tar pit(v1.4.1):非 WS 升级请求访问未知路径时挂起不响应——不返回 404、不泄露行为指纹,让路径扫描器空等;EasyTier 客户端只走升级、保留路径(health/favicon/metrics/admin)各有响应,均不受影响;0 = 关闭(恢复 404) |
NETWORK_SECRETS |
(空) | 可选 JSON 映射 {"网络名":"密钥"}。配置后服务端用 SipHash-1-3(与官方同一算法)校验客户端摘要,密钥不匹配直接拒绝 |
# 示例:校验两个网络的密钥
[vars]
NETWORK_SECRETS = "{\"myteam\":\"s3cret!\",\"family\":\"home123\"}"
不配置
NETWORK_SECRETS时,任何持有正确(网络名, 密钥)组合的客户端都可接入 ——与官方公共服务器public.easytier.cn的行为一致(不同密钥的网络天然隔离,互不可见)。
5.5 多房间分片(可选)
默认所有连接进同一个 DO 房间 "global"(同房间内按网络名+摘要自动分组隔离)。
需要物理隔离(例如不同租户)时:
[vars]
ROOM_HEADER = "X-Room" # 按请求头分片
# 或
ROOM_QUERY = "room" # 按 URL 查询参数分片,如 wss://et.example.com/?room=t1
5.6 KV 审计(记录 + 黑名单)
六类信息(网络分组 / 节点在线 / 路由信息 / 全局互联 / 连接列表 / 摘要注册表)的 连接与事件记录进 Workers KV,每类只占一条 KV 键(记录 7 条 + 黑名单 4 条,共 11 条); 管理端另有黑名单(分四类:节点 PeerId / 网络分组 / 摘要注册 / 客户端 IP)。 记录查询页支持按类型筛选,类型下拉首项为**「全部」**(跨类型按时间合并视图,无需额外 KV 键)。
启用步骤(可选;不配置 KV 绑定时功能优雅降级——记录与黑名单仅存 Durable Object storage,管理页功能不变):
# 1. 创建 KV namespace(免费计划自带)
npx wrangler kv namespace create AUDIT_KV
# 输出 id 后填入 wrangler.toml:
[[kv_namespaces]]
binding = "AUDIT_KV"
id = "<上面输出的 id>"
# 2. 重新部署
npx wrangler deploy
| 变量 | 默认值 | 说明 |
|---|---|---|
RECORD_FLUSH_MS |
600000 |
KV 镜像最小间隔(毫秒)。事件先写内存 + DO storage,按此间隔节流写 KV(控制免费额度写入量) |
RECORD_DEFAULT_LIMIT |
100 |
各类记录默认条数上限(管理页「记录设置」可单独调整) |
BLACKLIST_LIMIT |
1000 |
各类黑名单条数上限(硬设置) |
ADMIN_AUDIT |
1 |
硬设置:管理端审计开关。记录管理员登录(IP/时间)与操作;管理页不可关闭,admin 类记录不可删除(v1.3.0 起不再记录"查看"事件——自动刷新不刷屏、省写入) |
ADMIN_AUDIT_LIMIT |
200 |
管理端审计记录上限(硬设置) |
IP_BLOCK_CACHE_MS |
30000 |
边缘层 IP 黑名单缓存(v1.4.0):isolate 内缓存 bl:socket 的 KV 读结果,重连风暴时 KV 读从每次连接 1 次降为每 TTL 1 次。只延后拦截、绝不漏放(缓存未命中照旧放行走 DO 权威检查);0 = 关闭 |
AUDIT_IP_MASK |
0 |
审计 IP 打码(v1.6.0):1 时记录中的客户端 IP 打码——IPv4 保留 /16 前缀(1.2.x.x)、IPv6 保留前 4 组。仅影响记录展示(旧记录不回填);黑名单存储、踢人拉黑、边缘拦截仍使用完整 IP,功能不受影响 |
AUDIT_RETENTION_DAYS |
0 |
记录留存天数(v1.6.0):0 = 不限(默认,仅受各类条数上限约束);配置 N 后 alarm 每小时清理一次超过 N 天的运行记录(admin 硬审计豁免),清理数写入日志并同步 KV |
KV 键布局与写入策略详见技术文档 §9。免费额度核算:
KV 免费计划 1000 写/天——默认 10 分钟节流下每键理论最大 144 写/天,
实际仅在有新事件的周期产生写入(典型小网络全类合计每天数十次),远低于限额;
读取主要为两处——冷启动且 DO storage 为空时(每键一次)与 WebSocket 升级时的
边缘黑名单检查(只读 bl:socket 一条键,v1.4.0 起有 isolate 缓存:默认每 30s
至多一次 KV 读,重连风暴时也只消耗每 TTL 一次读取)。
黑名单行为:管理页执行「踢出节点 / 删除分组 / 删除摘要注册 / 断开连接」时, 操作对象(PeerId / 网络名 / IP)自动进入对应类别黑名单并被拦截后续接入; 黑名单页可手工添加、移除(解除封锁)或清空。断开连接会拉黑该客户端 IP—— 若多个节点共用同一出口 IP,请谨慎使用(可先移除该 IP 再让其重连)。
IP 黑名单为双层拦截(v1.3.0):Worker 入口先读 KV 直接 403(不唤醒 DO,
被拉黑客户端的重连风暴不消耗 DO 请求与时长计费),DO 层兜底。v1.4.0 起边缘
层 KV 读取带 isolate 缓存(IP_BLOCK_CACHE_MS,默认 30s),只延后拦截、
绝不漏放——缓存未命中(含 KV 读失败)照旧放行走 DO 层权威检查。因此:
- 解除 IP 封锁后,KV 传播完成前(通常数秒,最长约 1 分钟)+ 缓存 TTL 内, 边缘层可能仍拒绝该 IP;被新拉黑的 IP 在此窗口内会打到 DO 层被权威拦截;
- 黑名单拒绝不写入记录(防重连风暴刷爆记录列表),拒绝次数见总览页 「黑名单拦截」计数(DO 层口径;边缘层拒绝不经过 DO,不计数)。
事前限流(v1.4.1):黑名单是事后拦截(需管理员操作或攻击已发生),
单 IP 并发连接上限(MAX_CONNS_PER_IP,DO 升级层)补上事前缺口——同一 IP
的开放连接数达到上限后直接 429,无需先进入黑名单;拦截次数见总览页
「IP 限流」计数(ipLimited)。另:未知路径的非升级请求会被挂起不响应
(tar pit,TARPIT_UNKNOWN),不泄露 404 与行为指纹。
KV 键前缀(v1.4.1 起更名为 easytier-cf-relay:,此前为 et-relay:):
首次启动新版本时自动把旧键数据迁移到新键(旧键保留不删,确认运行正常后可
在 Cloudflare 控制台手动清理);无需任何手工操作。
多房间分片注意:KV 审计键为全局共享(各房间 DO 刷盘同一组键,以最后 刷盘的房间为准)。KV 审计与边缘 IP 拦截面向默认单房间部署;使用
ROOM_HEADER/ROOM_QUERY分片时不建议启用AUDIT_KV。
5.7 日志
| 变量 | 默认值 | 说明 |
|---|---|---|
LOG_LEVEL |
info |
debug / info / warn / error。debug 会打印每个包的类型与路由,仅排障时开启 |
5.8 监控与管理端
生产环境默认全部禁用(不配置即 404,零攻击面)。启用时务必使用 随机长路径 + 强 token 双重防线:
| 变量 | 默认值 | 说明 |
|---|---|---|
HEALTH_PATH |
/health |
健康检查路径(v1.4.0)。配置后仅该路径返回 {"ok":true},原 /health 不再暴露(404 或随 tar pit 挂起;无需 token) |
METRICS_PATH |
(空) | 统计端点自定义安全路径。不配置则 /metrics 完全禁用 |
METRICS_TOKEN |
(空) | 统计端点 token(Authorization: Bearer <t> 或 ?token=<t>)。与 METRICS_PATH 同时配置才启用(fail-closed)。⚠️ ?token= 会进入 URL 日志/浏览器历史/Referer——生产建议只用 Bearer 头(v1.4.2 起文档标注,兼容保留) |
ADMIN_PATH |
(空) | Web 管理端路径。与 ADMIN_TOKEN 同时配置才启用(页面壳公开、数据 API 鉴权) |
ADMIN_TOKEN |
(空) | 管理端 token(状态查看 / 踢出节点 / 删除分组)。安全审计建议:token 属敏感信息,生产建议 npx wrangler secret put ADMIN_TOKEN 注入而非写进 toml 明文(与 METRICS_TOKEN/CF_API_TOKEN 同模式) |
[vars]
HEALTH_PATH = "/h-a1b2c3d4" # 可选,默认 /health
METRICS_PATH = "/m-a1b2c3d4" # 用 openssl rand -hex 8 生成
METRICS_TOKEN = "长随机字符串"
ADMIN_PATH = "/console-e5f6a7"
ADMIN_TOKEN = "另一个长随机字符串"
要点:
- 鉴权失败统一返回 404(不暴露端点存在性);token 比较为常数时间(防时序侧信道);
- DO 内部端点(
/internal/*)只接受 Worker 入口鉴权后的改写转发:带Upgrade: websocket头伪造内部路径的请求在 DO 层直接 404(v1.4.1, 防「升级头 + 内部路径」绕过 token 直达端点); - 管理端页面壳本身无数据(可公开访问),所有数据 API 均需 token;
- token 属于敏感信息:生产建议用
npx wrangler secret put METRICS_TOKEN注入而非写进 toml; wrangler.toml中的[env.security]测试环境变量不会随默认wrangler deploy上传;- 管理页自动刷新间隔 30s(v1.4.0 起;且页面不可见时自动暂停),长开页面的请求量 约 2.9k/天,对免费额度(100k/天)占用很小,不需要时可关闭自动刷新; v1.6.0 起「额度观测 / 趋势」在服务端报告未配置 CF 凭证后,前端会记住状态并 跳过后续轮询(卡内提供「重新检测」入口),避免空转请求。
Prometheus 文本输出(v1.6.0):启用 /metrics 后,追加 ?format=prometheus
即输出 Prometheus text exposition format(text/plain; version=0.0.4),25 个
easytier_ 命名空间指标(无 label 防高基数):easytier_peers / easytier_groups /
easytier_uptime_seconds / easytier_sockets / easytier_sockets_handshaked(gauge)
与 easytier_msgs_in_total / easytier_msgs_out_total /
easytier_bytes_in_total / easytier_bytes_out_total / easytier_forwards_total /
easytier_conns_total / easytier_errors_total / easytier_forgeries_total /
easytier_blacklist_rejected_total / easytier_ip_limited_total /
easytier_rate_limited_total / easytier_group_limited_total /
easytier_alarms_total(counter)。抓取示例(自托管 Prometheus):
scrape_configs:
- job_name: easytier-relay
scrape_interval: 60s
metrics_path: /m-a1b2c3d4 # 你的 METRICS_PATH
params:
format: [prometheus]
authorization:
credentials: "<METRICS_TOKEN>"
static_configs:
- targets: ["<你的域名>"]
Grafana 可用 Prometheus 数据源直接画 easytier_peers、
rate(easytier_msgs_in_total[5m]) 等面板;拨测平台可继续用 JSON 端点或
/health。注意 counter 随 DO 重启归零(免费计划 DO 周期休眠回收),告警
建议基于 rate() / increase() 而非绝对值。
5.8.1 额度观测与趋势(v1.6,可选)
未配置时总览「额度观测」卡为自观测估算口径,行为与 v1.5 一致。配置下列项后 升级为账号级真实数据(全部可选,逐项独立生效):
| 项 | 类型 | 说明 |
|---|---|---|
CF_ACCOUNT_ID |
vars(非敏感) | Cloudflare 账号 ID(Dashboard 首页右侧 / Workers 概览页可见)。真实额度与 AE 趋势查询共用 |
CF_API_TOKEN |
secret | Cloudflare API Token,必须 npx wrangler secret put CF_API_TOKEN 注入,绝不写入 toml 明文。权限只给 Account → Account Analytics → Read(只读,可随时在 Dashboard → My Profile → API Tokens 吊销)。供 GraphQL 额度查询与 AE SQL 趋势查询共用 |
CF_SCRIPT_NAME |
vars(可选) | 被查询的 Worker 脚本名,默认 easytier-cf-relay;用其它名字部署时需同步 |
AE 绑定 |
wrangler.toml | Analytics Engine 绑定(趋势打点)。绑定即自动建数据集,无需控制台操作 |
AE_DATASET |
vars | 趋势 SQL 查询的 AE 数据集名,必须与 AE 绑定的 dataset 一致(AE 绑定无法在运行时读取自身 dataset 名)。默认 easytier-cf-relay(与 Worker 名一致);绑定省略 dataset 时 wrangler 默认取 Worker 名;绑定改名/自定名的部署两处须一起改,否则趋势查询静默返回空(有节点也无数据) |
[vars]
CF_ACCOUNT_ID = "你的账号 ID"
CF_SCRIPT_NAME = "easytier-cf-relay"
AE_DATASET = "easytier-cf-relay" # 与下方 dataset 一致
[[analytics_engine_datasets]]
binding = "AE"
dataset = "easytier-cf-relay"
# Token 一律 secret 注入(不要写在这里):
# npx wrangler secret put CF_API_TOKEN
行为说明:
- 真实额度(管理端
/api/quota):服务端出站调 GraphQL Analytics API 统计当日 UTC 窗口的workersInvocationsAdaptive请求数(出站调用不占 100k 入站额度);结果缓存 5 分钟(失败缓存 1 分钟),管理页 30s 自动刷新 不会打爆 API;查询失败自动回退自观测估算并在卡片标注原因。Token 只在 服务端使用,浏览器 / 日志 / 审计中不出现。 - 趋势图(管理端
/api/trends):DO 每 ≥60s 向 AE 写一个计数快照 (约 1,440 点/天,免费计划含 100,000 点/天,打点不占请求额度);查询走 AE SQL API,24h 窗口 15 分钟一桶、7d 窗口 1 小时一桶,SQL 分桶查询失败 自动回退原始点服务端聚合。未绑定 AE 时打点静默跳过、趋势卡提示未启用。 - GraphQL 免费计划可用性已于 v1.6 立项前复核(账号级 Workers 数据集可用; zone 级站点分析受限与本功能无关);如官方 API 变更导致查询失败,功能 自动降级为估算口径,不影响其它功能。
6. 客户端接入
6.1 Linux / macOS(easytier-core CLI)
easytier-core \
--network-name myteam \
--network-secret s3cret! \
--hostname node-a \
-p wss://et.example.com/
实测输出(v2.6.4 官方客户端 + 本项目):
INFO CORE::INSTANCE::CONNECTION: new peer connection added conn_info=PeerConnInfo { my_peer_id: ..., dst_peer_id: 10000001, ... }
INFO CORE::INSTANCE: new peer added peer_id=10000001
$ easytier-cli peer
| ipv4 | hostname | cost | lat(ms) | tunnel | version |
|---------------|----------|-------|---------|--------|-------------------|
| 10.126.0.1/24 | nodeA | Local | - | - | 2.6.4-8428a89d |
| | easytier-cf-relay | p2p | 3.88 | ws | easytier-cf-relay/1.0.0 |
| 10.126.0.2/24 | nodeB | relay(2) | 18.00 | | 2.6.4-8428a89d |
6.2 Windows GUI / macOS GUI / 手机端
- 网络名称:
myteam - 网络密钥:
s3cret! - 节点/Peer:
wss://et.example.com/(新建一个节点条目填入即可)
6.3 Docker
docker run --name easytier -d --network host --cap-add NET_ADMIN \
easytier/easytier:latest \
--network-name myteam --network-secret 's3cret!' \
-p wss://et.example.com/
6.4 兼容性矩阵
| 客户端 | 结果 |
|---|---|
| easytier-core 2.6.4 Linux x86_64 | ✅ 实测通过(握手/路由/双节点互见/数据中继/长时间稳定) |
| easytier-core 2.x 其它版本 | ✅ 协议层面兼容(握手 + OSPF + PeerCenter 均按官方 2.6.x 语义实现) |
| secure mode(Noise,v2.6.0+ 可选功能) | ❌ 不支持,客户端需使用默认(非 secure)模式 |
7. 成本与额度
免费计划即可运行本服务(SQLite 后端 Durable Objects 已对免费计划开放)。关键额度:
| 维度 | 免费计划 | 说明 |
|---|---|---|
| 请求 | 100,000 / 天 | 含 WS 连接建立、入站 WS 消息(20:1 折算)、alarm 唤醒 |
| Duration | 13,000 GB-s / 天 | Hibernation 状态不计费 —— 本实现全用 Hibernation API,空闲时成本趋近零 |
| 存储 | 5 GB / 账户 | 本服务仅存路由元数据(KB 级) |
| KV 写 | 1,000 / 天 | 启用 KV 审计后:事件按 RECORD_FLUSH_MS(默认 10 分钟)节流镜像,每键理论最大 144 写/天;拒绝/查看事件不产生写入 |
| KV 读 | 100,000 / 天 | 冷启动且 DO storage 为空时读取(每键一次)+ WS 升级的边缘黑名单检查(v1.4.0 起 isolate 缓存,默认每 30s 至多 1 次读) |
| 单 DO 吞吐 | ~1,000 req/s(软限制) | 单房间 64 peer 远达不到 |
成本估算(10 个客户端、每客户端 8s 一次心跳的典型小网络):
- 计费请求 ≈ 10 × (86400/8) / 20 ≈ 5,400 请求/天(v1.4.0 起 alarm 自适应 排程 + 空闲退避:有连接时 alarm 对齐最近事件时刻、至多 15s 一次;空房间退避 到 5 分钟一次 = 288 请求/天,较固定 15s 的 5,760 请求/天大幅下降);
- duration:连接空闲即休眠,活跃处理时间每天累计不足 1 GB-s;
- KV 审计:每天几十次连接波动 × 涉及的 2~3 个键 ÷ 10 分钟节流合并 ≈ 数十写/天, 远低于 1000/天限额(记录上限 100 条/类,存储占用 KB 级);
- KV 读 ≈ 冷启动恢复 + 边缘黑名单检查(缓存后每 30s 至多 1 次)≈ 数千读/天, 远低于 10 万限额;
- 被拉黑客户端的重连风暴:每次重连仍占 1 次 Worker 请求(客户端行为), 但 v1.3.0 起在 Worker 入口边缘层直接 403,不唤醒 DO、不产生审计写入, DO 请求与 KV 写入均零消耗;v1.4.0 起边缘层 KV 读有 30s 缓存, 风暴期间 KV 读也不再与重连次数成正比;
- 结论:免费额度内稳定运行。超出时可升级 Workers Paid($5/月起,含 100 万请求/月 + 40 万 GB-s/月)。
计费细节参见 Cloudflare 官方文档:
8. 运维:日志、监控、升级
8.1 健康检查与指标
# 健康检查:零指纹(P0 整改后仅返回 {"ok":true})
curl https://et.example.com/health
# {"ok":true}
# 配置 HEALTH_PATH 后:curl https://et.example.com<HEALTH_PATH>(原 /health 不再暴露:404 或 tar pit 挂起)
# 统计:自定义安全路径 + token(未配置 METRICS_PATH/METRICS_TOKEN 时 404)
curl -H "Authorization: Bearer <METRICS_TOKEN>" https://et.example.com<m-metrics-path>
# {
# "ok": true, "totalPeers": 2, "groupCount": 1,
# "groups": { "myteam:<digest>": { "peers": [...], "knownInfos": 3 } },
# "counters": { "msgsIn": 103, "msgsOut": 103, "forwards": 58, "errors": 0, ... }
# }
字段速查:forwards = 数据包中继次数;errors = 协议错误数(应恒为 0);
forgeries = 源伪造拦截数(F-06 防线命中);groups 按网络名分组列出在线 peer ID。
8.2 Web 管理端
配置 ADMIN_PATH + ADMIN_TOKEN 后,浏览器打开 https://et.example.com<ADMIN_PATH>
进入控制台(输入 token 即可,浏览器本地保存)。
界面结构(侧边栏列表式导航,按功能块分区):
- 总览(对照
easytier-cli stats/node):运行时长(自房间创建起累计,含休眠)、分组数、 在线节点、收发包/字节/转发/错误计数,以及审计概览(记录/黑名单各分类计数与 KV 状态); - 节点在线(对照
easytier-cli peer):各分组在线节点、连接时间、最近活动,可踢出节点; - 路由信息(对照
easytier-cli route):RoutePeerInfo 路由表,含 direct/transit 来源标记 与幽灵条目标记(transit 来源 + 无连接),可删除条目; - 全局互联(对照
easytier-cli peer-center):PeerCenter 互联表,可删除条目; - 连接列表:全部 socket(含未握手),含 socket ID 与客户端 IP,可断开连接;
- 网络分组(对照
easytier-cli foreign-network):按网络名+摘要隔离的分组,可删除分组; - 摘要注册表:网络名 → 摘要注册(STRICT_DIGEST 判定依据),可删除注册;
- 记录查询:七类记录(六类数据 + 管理端审计)分页浏览;类型下拉首项为 「全部」(跨类型按时间合并,附类型列),单条/批量删除与清空 (admin 类为硬审计,只读不可删——"全部"视图中 admin 行的勾选与删除均禁用);
- 记录设置:六类数据记录各自的开/关与条数上限(管理端审计与黑名单上限为硬设置,只读展示);
- 黑名单:四类黑名单(节点 PeerId / 网络分组 / 摘要注册 / 客户端 IP)分页浏览, 手工添加、单个/批量移除(解除封锁)与清空。
所有列表的**「操作」列按钮针对单行直接生效**(无需先勾选复选框;勾选用于顶部 批量按钮的批量操作)。总览页统计含「黑名单拦截」计数(DO 层拒绝次数, 用于观测被拉黑客户端的重连行为——拒绝事件本身不再写入记录)。
性能设计(节点多时页面不爆炸):
- 服务端分页:所有列表
?tab=&offset=&limit=分页拉取,每页默认 50 条(可选 20/50/100/200,上限 200); - 独立滚动:列表区固定最大高度、内部滚动,表头 sticky;
- 按块统计:每个功能块头部显示自身统计(总数/直连/中转/幽灵/空分组等), 侧边栏实时显示各块计数;
- 批量操作:每个功能块支持勾选(单选/全选本页)+ 批量按钮 (删除分组 / 踢出节点 / 删除路由 / 删除互联 / 断开连接 / 删除摘要注册);
- 自动刷新仅拉取当前 tab(30s,可关闭;页面不可见时自动暂停),开销可控。
删除分组:断开组内全部连接、清除路由/会话数据、解除摘要注册 (被恶意摘要抢占时可用此操作解锁)。注意:删除分组 / 踢出节点 / 删除摘要注册 / 断开连接会把操作对象(网络名 / PeerId / IP)自动加入对应类别的黑名单并拦截 后续接入,需要放行时到「黑名单」页移除即可(见 5.6 节)。
官方 CLI 中依赖 UDP/TUN/系统服务的命令(connector、vpn-portal、stun、acl 等)
在 Workers 运行时不可用,控制台底部有完整对照表。
也可直接调 API(鉴权方式同上):
# 查看状态(tab: overview/groups/peers/routes/peercenter/sockets/digests;分页 offset/limit)
curl -H "Authorization: Bearer <ADMIN_TOKEN>" \
"https://et.example.com<ADMIN_PATH>/api/state?tab=routes&offset=0&limit=50"
# 踢出节点(单个或批量)
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "content-type: application/json" \
-d '{"peers":[{"groupKey":"myteam:<digest>","peerId":12345678}]}' \
https://et.example.com<ADMIN_PATH>/api/peer/kick
# 删除分组(支持 groupKeys 数组 / networkName / all)
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "content-type: application/json" \
-d '{"networkName":"myteam"}' \
https://et.example.com<ADMIN_PATH>/api/group/delete
# 删除路由条目(批量;在线节点的 direct 条目会被跳过)
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "content-type: application/json" \
-d '{"groupKey":"myteam:<digest>","peerIds":[12345678,12345679]}' \
https://et.example.com<ADMIN_PATH>/api/route/delete
# 删除互联表条目(批量)
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "content-type: application/json" \
-d '{"groupKey":"myteam:<digest>","peerIds":[12345678]}' \
https://et.example.com<ADMIN_PATH>/api/peercenter/delete
# 断开指定连接(批量,按 socketId)
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "content-type: application/json" \
-d '{"socketIds":[3,5]}' \
https://et.example.com<ADMIN_PATH>/api/socket/close
# 删除摘要注册(批量;连带清除使用该摘要的分组并断开其连接)
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "content-type: application/json" \
-d '{"networkNames":["myteam"]}' \
https://et.example.com<ADMIN_PATH>/api/digest/delete
# ---- KV 审计(记录 + 黑名单)----
# 查询记录(type: groups/peers/routes/peercenter/sockets/digests/admin;分页)
curl -H "Authorization: Bearer <ADMIN_TOKEN>" \
"https://et.example.com<ADMIN_PATH>/api/records?type=peers&offset=0&limit=50"
# 删除记录(ids 为记录 id 数组或 "all";admin 类返回错误——硬审计不可删)
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "content-type: application/json" \
-d '{"type":"peers","ids":"all"}' \
https://et.example.com<ADMIN_PATH>/api/records/delete
# 读取 / 修改记录配置(每类开关 + 上限;admin 类不在可改范围)
curl -H "Authorization: Bearer <ADMIN_TOKEN>" \
"https://et.example.com<ADMIN_PATH>/api/record/config"
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "content-type: application/json" \
-d '{"types":{"peers":{"on":true,"limit":200}}}' \
https://et.example.com<ADMIN_PATH>/api/record/config
# 查询黑名单(cat: peer/group/digest/socket)
curl -H "Authorization: Bearer <ADMIN_TOKEN>" \
"https://et.example.com<ADMIN_PATH>/api/blacklist?cat=peer&offset=0&limit=50"
# 手工拉黑(cat + value;peer 为数字 PeerId,group/digest 为网络名,socket 为 IP)
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "content-type: application/json" \
-d '{"cat":"peer","value":12345678,"reason":"abuse"}' \
https://et.example.com<ADMIN_PATH>/api/blacklist/add
# 移除黑名单(解除封锁;ids 为条目 id 数组或 "all")
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "content-type: application/json" \
-d '{"cat":"peer","ids":"all"}' \
https://et.example.com<ADMIN_PATH>/api/blacklist/delete
8.3 实时日志
npx wrangler tail --format pretty
LOG_LEVEL=debug 时可看到每个包 msg type=N from=X to=Y 与 RPC 分发详情。
8.4 升级
git pull # 或修改代码
npx wrangler deploy
部署即热更新。已连接的 WebSocket 会经历一次重连(客户端自动重试,秒级恢复)。 房间路由状态持久化在 DO storage,升级不丢。
8.5 Durable Object 迁移注意
本工程 wrangler.toml 使用:
[[migrations]]
tag = "v1"
new_sqlite_classes = ["RelayRoom"]
- 全新部署:保持原样即可。
- 接手已有
new_classes(KV 后端)的旧部署:不要删掉旧 migration 条目,改为追加:
[[migrations]]
tag = "v1"
new_classes = ["RelayRoom"] # 旧条目原样保留
[[migrations]]
tag = "v2"
new_sqlite_classes = ["RelayRoom"] # 追加:切换到 SQLite 后端
8.6 重新生成 Protobuf 静态代码(仅开发)
跟随官方 .proto 变更时使用(普通用户不需要):
bash tools/regen_proto.sh # 需要本地有官方源码 checkout
9. 故障排查 FAQ
Q1:客户端一直 connect timeout after 20s?
curl https://你的域名/health确认 Worker 存活;- 检查 URL scheme 是
wss://(而非ws://,Workers 强制 TLS); npx wrangler tail看握手是否到达。到达但被拒(digest mismatch)说明密钥不对。
Q2:客户端握手成功但 easytier-cli peer 看不到服务端节点?
通常是旧版本第三方实现的问题;本项目已实测 2.6.4 正常显示(hostname=easytier-cf-relay)。
若遇到,开启 LOG_LEVEL=debug 检查 rpc req ... service=OspfRouteRpc 日志行,
存在但无响应时请提 issue 附日志。
Q3:节点显示在线,但互相 ping 不通?
- 确认两端
--network-name与--network-secret完全一致(STRICT_DIGEST=1 时不同摘要会被拒绝,日志有digest mismatch); RELAY_DATA=0时中继不转发数据面,必须依赖 P2P 直连(打洞失败的组合无法通信),需要兜底请设RELAY_DATA=1。
Q4:本地 wrangler dev 报 Address already in use?
端口被占用:npx wrangler dev --port 8788 换端口,或杀掉旧进程。
Q5:wrangler deploy 报 DO 类已存在 / migration 冲突?
见 8.4 迁移章节。切勿删除 wrangler.toml 中已有的 [[migrations]] 条目。
Q6:两个客户端都能连上,但互相看不到对方(只看得到 easytier-cf-relay)?
检查是否误配置了 ROOM_HEADER/ROOM_QUERY——两个客户端进了不同 DO 房间。
同房间要求相同的 URL/Header。
Q7:想统计流量?
配置 METRICS_PATH + METRICS_TOKEN 后访问统计端点,
counters.bytesIn/bytesOut/forwards 即为控制面+数据面的转发统计;或用 Web 管理端总览页。
Q8:同名网络被恶意抢占(digest mismatch)怎么办?
已配置 NETWORK_SECRETS 的网络不受影响(服务端以正确密钥摘要为准,注册表自愈)。
未配置的网络被抢占时,用管理端删除该分组即可解除注册,随后正常客户端可重新接入。
v1.6.0 起,此类握手被拒会写入 squat-suspect 审计事件(含攻击源 IP,
按网络名+IP 10 分钟去重)——「记录查询 → 摘要注册」页可见抢占者并可拉黑其 IP;
总览页「摘要抢占嫌疑」计数持续偏高即说明有人在尝试堵门。加固建议:公开部署
强烈建议配置 NETWORK_SECRETS(服务端密钥校验,抢占直接失效)。
Q9:如何复跑安全测试?
npx wrangler dev --env security --port 8788 # 使用 .dev.vars.security 中的测试 token
node test/security_test.mjs # 45 项黑盒安全测试
node test/audit_e2e_test.mjs # 34 项 KV 审计(记录/黑名单/边缘拦截)动态测试
Q10:踢出/删除的节点或网络重连不上? 这是黑名单在生效:踢出节点 / 删除分组 / 删除摘要注册 / 断开连接会把操作对象 (PeerId / 网络名 / IP)加入对应类别黑名单并拦截后续接入。 到管理端「黑名单」页移除对应条目(解除封锁)即可重新接入。 注意两点:
- 断开连接拉黑的是客户端 IP——若多个节点共用同一出口 IP,会一并被拦, 需要时先移除该 IP 条目;
- 配置了
AUDIT_KV时,IP 拦截发生在 Worker 入口边缘层(KV 直读)。 解除封锁后 KV 传播完成前(通常数秒,最长约 1 分钟)边缘层可能仍拒绝该 IP, 稍候即可。
Q11:管理端记录为什么删不掉 / 关不掉?"查看"事件为什么不见了?
管理端审计(登录 IP/时间、操作)是硬设置(wrangler.toml ADMIN_AUDIT),
管理页不可关闭、admin 类记录不可删除(防审计自擦除)。
六类数据记录则可在「记录设置」页单独开关并设置上限。
v1.3.0 起管理端"查看"事件不再记录(管理页自动刷新会持续刷屏且徒增写入);
黑名单拒绝事件同样不再写入「节点在线」记录(被拉黑客户端会不断重连,逐条记录
会刷爆列表),拒绝次数改看总览页「黑名单拦截」计数。
Q12:被拉黑的客户端一直重连,会不会刷爆免费额度?
每次重连尝试本身占用 1 次 Worker 请求(客户端行为,服务端无法避免),但
v1.3.0 起:拒绝发生在 Worker 入口边缘层(读 KV 的 bl:socket 一条键),
不再唤醒 Durable Object——不消耗 DO 请求与时长计费,也不产生任何审计写入
(否则重连风暴每秒都会产生记录与 KV 镜像写入)。v1.4.0 起该 KV 读带
isolate 缓存(IP_BLOCK_CACHE_MS,默认 30s):重连风暴时 KV 读从每次连接
1 次降为每 30s 至多 1 次,只延后拦截、绝不漏放(DO 层权威兜底)。
若确有恶意 IP 长期轰炸,可在 Cloudflare 控制台配置 WAF/IP Access Rules 在更外层拦截。
本手册对应实现版本:easytier-cf-relay/1.4.0(协议对齐 easytier-core 2.6.x)。技术实现细节见《技术文档》。