本文把一条原始的机场订阅链接,通过一个 Cloudflare Worker,转换成形如
https://sub.example.com/?token=YOUR_TOKEN 的可访问地址:访问时实时拉取机场订阅、
解析节点、套上你自己定制的一整套 Mihomo 规则(DNS / 分流 / TUN),并用 Token 做访问鉴权。
全程零服务器、免费额度足够个人使用。文中所有敏感信息已脱敏,替换为占位符。

一、背景:为什么要做订阅转换

大多数机场(本文以 JustMySocks 为例)给你的「订阅地址」长这样:

1
https://jmssub.net/members/getsub.php?service=XXXXXXX&id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

它返回的是一段 Base64 编码的纯节点列表,解码后是若干行 ss://...vmess://...
直接丢进客户端能用,但有几个不爽的点:

  1. 只有节点,没有规则。没有 DNS 防泄漏、没有国内外分流、没有去广告,体验全靠客户端默认。
  2. 订阅地址又长又敏感,还带着 service / id 这种明文凭证,不好记也不好分享给自己的其它设备。
  3. 想统一配置。我希望所有设备拉同一份「我精心调过的 Mihomo 模板」,节点部分自动从机场同步。

解决思路:写一个边缘函数做中间层 —— 客户端访问我的地址,函数去机场拉原始订阅,
解析出节点,再拼进我自己的一份完整 Mihomo 配置后返回。顺便:

  • 用一个好记的自有域名 + Token 控制访问;
  • 敏感信息(机场地址、Token)放在 KV 里,不写进代码
  • 改配置、换机场、换 Token 都不用重新发版。

为什么选 Cloudflare Workers

  • 免费额度对个人足够(每天 10 万次请求);
  • 全球边缘节点,延迟低;
  • 自带 KV 键值存储,存敏感配置正合适;
  • 自定义域名免费,且自动签发 HTTPS 证书;
  • 完全 Serverless,不用买服务器、不用运维。

二、最终效果

部署完成后,把下面这一条地址填进 Clash Verge / Mihomo / Stash 等客户端的「订阅」即可:

1
https://sub.example.com/?token=YOUR_TOKEN
  • 带正确 token → 返回一份完整的 Mihomo YAML(节点 + DNS + 规则);
  • Token 错误或缺失 → 返回 401 Unauthorized

三、整体架构与请求流程

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
┌─ 客户端 (Clash / Mihomo)

│ GET https://sub.example.com/?token=xxx

┌─ Cloudflare Worker @ sub.example.com ───────────────────────────

│ ① 校验 token:与 KV 中的 TOKEN 比对
│ 不一致 ─▶ 返回 401 Unauthorized

│ ② 一致 → 从 KV 读取 URL / URL_BACKUP

│ ③ 带 User-Agent 拉取机场订阅 ──▶ 机场订阅源 (jmssub.net)
│ ◀── 返回 Base64 文本

│ ④ atob 解码 → 得到 ss:// / vmess:// 节点列表

│ ⑤ 解析节点 + 套用你的 Mihomo 模板(DNS / 分流 / TUN)

│ ⑥ 返回完整 YAML(Content-Type: text/yaml)

└─ 客户端导入订阅,开始使用

◆ Workers KV(敏感配置隔离存储,代码里不含任何明文)
TOKEN 访问令牌(决定 ?token= 的值)
URL 机场订阅地址(主)
URL_BACKUP 机场订阅地址(备用,可选)

核心数据流:客户端 → 鉴权 → 读 KV 拿机场地址 → 拉原始订阅 → 解码 → 解析节点 → 渲染模板 → 返回
敏感配置全部隔离在 KV 中,代码本身不含任何机场信息或 Token。

四、前置条件

条件 说明
Cloudflare 账号 免费即可
一个已托管在 Cloudflare 的域名 域名的 zone 状态必须是 Active(即 NS 已指向 Cloudflare)。任意便宜/免费域名都行
本地 Node.js ≥ 18,推荐 20 / 22
机场订阅地址 本文以 JustMySocks 的 getsub.php 地址为例

关于域名:在 Cloudflare 控制台「添加站点」,按提示把域名在注册商处的 NS 改成 Cloudflare
分配的两个 nameserver,等 zone 状态变为 Active 即可。本文用 example.com 作 zone,
子域 sub.example.com 作为订阅地址。

五、初始化项目

1
2
3
mkdir jms-sub && cd jms-sub
npm init -y
npm install -D wrangler@latest

目标目录结构:

1
2
3
4
5
6
jms-sub/
├── src/
│ └── index.js # Worker 主逻辑
├── wrangler.jsonc # Worker 配置(KV 绑定 + 自定义域名)
├── package.json
└── .gitignore

.gitignore(避免把本地状态/密钥提交上去):

1
2
3
4
5
node_modules/
.wrangler/
.dev.vars
.env
.DS_Store

六、编写 Worker(核心代码讲解)

完整代码见文末 附录 A,这里拆开讲关键部分。

6.1 入口与 Token 鉴权

1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const token = url.searchParams.get("token");
const validToken = await env.SUBSCRIPTION_URL.get("TOKEN");

// KV 里设了 TOKEN 时才鉴权;不一致直接拒绝
if (validToken && token !== validToken) {
return new Response("Unauthorized", { status: 401 });
}
// ...继续拉取/解析/渲染
},
};

env.SUBSCRIPTION_URL 是后面绑定的 KV 命名空间。这里有个要点:
如果 KV 里没设 TOKEN,则不鉴权、谁都能访问——所以一定要记得写入 TOKEN

6.2 拉取机场原始订阅(KV 取地址 + Base64 解码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function fetchSubscription(env) {
const primaryUrl = await env.SUBSCRIPTION_URL.get("URL");
const backupUrl = await env.SUBSCRIPTION_URL.get("URL_BACKUP");
const urls = [primaryUrl, backupUrl].filter(Boolean);

for (const url of urls) {
try {
// 带一个常规 UA,避免部分订阅服务对空 UA 返回非订阅内容
const resp = await fetch(url, {
headers: { "User-Agent": "clash-verge/v2.0.0" },
cf: { cacheTtl: 0 },
});
if (!resp.ok) continue;
const content = atob((await resp.text()).trim()); // 机场返回 Base64
if (content) return { content, error: null };
} catch {}
}
return { content: null, error: "All subscription URLs failed" };
}

要点:

  • 机场订阅返回的是 Base64,用 atob() 解码成多行 ss:// / vmess://
  • User-Agent 很重要——不少机场对空 UA 会返回网页/错误而不是订阅;
  • 支持主备地址(URL / URL_BACKUP),主地址挂了自动回退。

6.3 解析节点

机场常见两种协议:Shadowsocks 与 VMess。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ss://Base64(method:password@server:port)#name
function parseSS(line) {
const [main, name] = line.slice("ss://".length).split("#");
const [auth, serverInfo] = atob(main).split("@");
const [method, password] = auth.split(":");
const [server, port] = serverInfo.split(":");
return { name: getCleanName(name), protocol: "ss",
server, port: +port, cipher: method, password };
}

// vmess://Base64(JSON)
function parseVMess(line) {
const cfg = JSON.parse(atob(line.slice("vmess://".length)));
return { name: getCleanName(cfg.ps), protocol: "vmess",
server: cfg.add, port: +cfg.port, uuid: cfg.id,
alterId: +(cfg.aid || 0), cipher: cfg.scy || "auto",
tls: cfg.tls === "tls", network: cfg.net || "tcp" };
}

注意:这里按 JustMySocks 的传统 SS 格式(整段 method:pass@host:port 都在 Base64 里)解析。
不同机场的 ss:// 可能是 SIP002 格式(ss://Base64(userinfo)@host:port),需相应调整 parseSS

6.4 渲染 Mihomo 模板

把解析出的节点拼进一份你自己的完整配置——这部分是「私货」,可以尽情定制:
mixed-porttundns(fake-ip + 防泄漏)、proxy-groups(url-test 自动测速)、
rules(GEOSITE/GEOIP 分流、去广告)等。示例还顺手拉取了 Tailscale 的 DERP 节点 IP,
*.ts.net 加直连规则(用不到可删)。完整模板见附录 A。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function generateClashYaml(proxies) {
const proxiesList = proxies.map(p => /* 渲染每个节点 */).join("\n\n");
const proxyNames = proxies.map(p => ` - "${p.name}"`).join("\n");
return `mixed-port: 7890
# ...省略:tun / dns / 等全局配置...
proxies:
${proxiesList}

proxy-groups:
- name: PROXY
type: url-test
url: http://www.gstatic.com/generate_204
interval: 180
proxies:
${proxyNames}

rules:
- GEOSITE,category-ads-all,REJECT
- GEOSITE,CN,DIRECT
- GEOIP,CN,DIRECT
- MATCH,PROXY
`;
}

七、配置 wrangler.jsonc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "jms-sub",
"main": "src/index.js",
"compatibility_date": "2026-06-26",
"observability": { "enabled": true },

// KV:Worker 用 env.SUBSCRIPTION_URL 读取 TOKEN / URL / URL_BACKUP
"kv_namespaces": [
{ "binding": "SUBSCRIPTION_URL", "id": "<YOUR_KV_NAMESPACE_ID>" }
],

// 自定义域名:把 Worker 绑到 sub.example.com
"routes": [
{ "pattern": "sub.example.com", "custom_domain": true }
]
}
  • binding 名(SUBSCRIPTION_URL)必须和代码里的 env.SUBSCRIPTION_URL 完全一致;
  • id 占位符在下一步创建 KV 后填入;
  • compatibility_date 用近期日期即可。

八、创建 KV 并写入配置

8.1 登录

1
2
npx wrangler login   # 浏览器授权
npx wrangler whoami # 确认账号

8.2 创建 KV 命名空间

1
npx wrangler kv namespace create SUBSCRIPTION_URL

输出里会给出一个 id,形如:

1
{ "binding": "SUBSCRIPTION_URL", "id": "0123456789abcdef0123456789abcdef" }

把这个 id 填回 wrangler.jsonckv_namespaces[0].id

8.3 写入 Token 与订阅地址(写到远端 KV)

1
2
3
4
5
6
7
8
9
10
11
12
NSID="0123456789abcdef0123456789abcdef"   # 换成你的 KV id

# 访问 Token(决定最终 URL 里的 ?token= 值,建议随机生成,见下)
npx wrangler kv key put --namespace-id "$NSID" --remote \
"TOKEN" "YOUR_TOKEN"

# 机场订阅地址(含 & ,整段务必加引号)
npx wrangler kv key put --namespace-id "$NSID" --remote \
"URL" "https://jmssub.net/members/getsub.php?service=XXXXXXX&id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# 可选:备用订阅地址
# npx wrangler kv key put --namespace-id "$NSID" --remote "URL_BACKUP" "https://备用地址"

随机生成一个 Token:

1
openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 32; echo

要点:写远端 KV 一定带 --remote;不带 --remote(或带 --local)写的是本地 dev 的模拟存储。

九、绑定自定义域名

wrangler.jsonc 里已经声明:

1
"routes": [{ "pattern": "sub.example.com", "custom_domain": true }]

前提:example.com 这个 zone 已经在你的 Cloudflare 账号里且状态 Active。
满足后,部署时 custom_domain: true 会自动创建 sub.example.com 的路由并签发证书。

暂时没有自定义域名也能用:删掉整个 routes 段,部署后用 Cloudflare 自动分配的
https://jms-sub.<你的子域>.workers.dev/?token=... 访问。

十、部署与验证

1
npx wrangler deploy

成功输出里会看到:

1
2
Deployed jms-sub triggers
sub.example.com (custom domain)

验证(自定义域名首次可能要等几十秒签发证书):

1
2
3
4
5
# 正确 token → 200,返回一大段 YAML
curl -s "https://sub.example.com/?token=YOUR_TOKEN" | head -12

# 错误 token → 401 Unauthorized
curl -s -w " <- HTTP %{http_code}\n" "https://sub.example.com/?token=wrong"

看到 YAML 开头的 mixed-port: 7890 ... 和错误 token 的 401,即大功告成。
把订阅地址填进客户端即可。

十一、踩坑记录(真实遇到的)

  1. 自定义域名返回 Hello World! 或别人的内容
    该 hostname 之前被另一个 Worker 的路由 / 自定义域名占用了。
    去 Cloudflare 控制台 → 旧 Worker → Settings → Domains & Routes,把对应绑定删掉,再重新 deploy

  2. dig sub.example.com 解析出 198.18.x.x
    这是本机正在运行的 Clash/Mihomo 的 fake-ip,不是真实解析,属正常现象。
    想看真实情况可以换台没开代理的设备,或用 curl --resolve 指定 Cloudflare 真实 IP。

  3. 拉取订阅失败 / 返回空

    • fetchUser-Agent(很多机场对空 UA 返回网页);
    • 确认订阅地址直接访问返回的是 Base64(不是 JSON / HTML);
    • 配上 URL_BACKUP 备用域名兜底。
  4. Token 不生效,谁都能访问
    KV 里没写 TOKEN。代码逻辑是「TOKEN 为空则不鉴权」,务必写入。

  5. kv key put 写错了位置
    写远端要带 --remote--local 写的是 wrangler dev 的本地模拟存储,部署后读不到。

  6. ss:// 解析不出节点
    你的机场可能用 SIP002 格式,需要按 6.3 的提示改写 parseSS

十二、日常维护:换机场 / 换 Token 不用重新发版

配置都在 KV 里,改完即时生效,无需 deploy

1
2
3
NSID="你的 KV id"
npx wrangler kv key put --namespace-id "$NSID" --remote "URL" "新的机场订阅地址"
npx wrangler kv key put --namespace-id "$NSID" --remote "TOKEN" "新的Token"

只有改了 src/index.js(模板 / 解析逻辑)才需要重新 npx wrangler deploy
排错看实时日志:npx wrangler tail

十三、安全与成本

  • Token 是访问凭证,别写进公开仓库、别截图泄露;想吊销就改 KV 里的 TOKEN
  • Worker 输出的仍是明文 Mihomo 配置(含节点凭证),靠 HTTPS 传输保护,请勿把订阅地址公开分享。
  • wrangler.jsonc 里的 KV id 不是机密,但 Token / 机场地址在 KV 里,不要把它们硬编码进代码或提交到 Git。
  • 成本:Workers 免费版每天 10 万请求、KV 免费版每天 10 万次读,个人用绰绰有余。

附录 A:完整 src/index.js

直接复制即可使用。代码本身不含任何敏感信息——机场地址与 Token 全部来自 KV。

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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
const PROTOCOL_SS_PREFIX = "ss";
const PROTOCOL_VMESS_PREFIX = "vmess";

const PROTOCOL_SS = PROTOCOL_SS_PREFIX + "://";
const PROTOCOL_VMESS = PROTOCOL_VMESS_PREFIX + "://";

export default {
async fetch(request, env, ctx) {
try {
const url = new URL(request.url);
const token = url.searchParams.get("token");
const validToken = await env.SUBSCRIPTION_URL.get("TOKEN");

if (validToken && token !== validToken) {
return new Response("Unauthorized", { status: 401 });
}

const { content, error } = await fetchSubscription(env);
if (error) {
return new Response("Error: " + error, { status: 500 });
}

const lines = content.split(/\r?\n/).filter((line) => line.trim());
const proxies = [];
for (const line of lines) {
if (line.startsWith(PROTOCOL_SS)) {
const proxy = parseSS(line);
if (proxy) proxies.push(proxy);
} else if (line.startsWith(PROTOCOL_VMESS)) {
const proxy = parseVMess(line);
if (proxy) proxies.push(proxy);
}
}

if (proxies.length === 0) {
return new Response("Error: No valid proxies found", { status: 500 });
}

const yaml = await generateClashYaml(proxies);
return new Response(yaml, {
headers: {
"Content-Type": "text/yaml; charset=utf-8",
"Content-Disposition": 'inline; filename="mihomo.yaml"',
},
});
} catch (e) {
return new Response("Internal Error: " + e.message, { status: 500 });
}
},
};

async function fetchSubscription(env) {
const primaryUrl = await env.SUBSCRIPTION_URL.get("URL");
const backupUrl = await env.SUBSCRIPTION_URL.get("URL_BACKUP");
const urls = [primaryUrl, backupUrl].filter(Boolean);

if (urls.length === 0) {
return { content: null, error: "No subscription URL configured" };
}

for (const url of urls) {
try {
const resp = await fetch(url, {
headers: { "User-Agent": "clash-verge/v2.0.0" },
cf: { cacheTtl: 0 },
});
if (!resp.ok) continue;

const rawBody = await resp.text();
const content = atob(rawBody.trim());
if (content && content.length > 0) {
return { content, error: null };
}
} catch {}
}

return { content: null, error: "All subscription URLs failed" };
}

async function fetchTailscaleData() {
try {
const resp = await fetch("https://login.tailscale.com/derpmap/default");
if (!resp.ok) return { ipv4: [], ipv6: [] };

const data = await resp.json();
const regions = data && data.Regions ? data.Regions : {};
const ipv4 = new Set();
const ipv6 = new Set();

for (const region of Object.values(regions)) {
const nodes = Array.isArray(region.Nodes) ? region.Nodes : [];
for (const node of nodes) {
if (node.IPv4) ipv4.add(node.IPv4);
if (node.IPv6) ipv6.add(node.IPv6);
}
}
return { ipv4: [...ipv4], ipv6: [...ipv6] };
} catch {
return { ipv4: [], ipv6: [] };
}
}

function parseSS(line) {
try {
const ssStr = line.slice(PROTOCOL_SS.length);
const [main, name] = ssStr.split("#");
const [auth, serverInfo] = atob(main).split("@");
const [method, password] = auth.split(":");
const [server, port] = serverInfo.split(":");
return {
name: getCleanName(name),
protocol: PROTOCOL_SS_PREFIX,
server,
port: parseInt(port),
cipher: method,
password,
};
} catch {
return null;
}
}

function parseVMess(line) {
try {
const vmessStr = atob(line.slice(PROTOCOL_VMESS.length));
const config = JSON.parse(vmessStr);
return {
name: getCleanName(config.ps),
protocol: PROTOCOL_VMESS_PREFIX,
server: config.add,
port: parseInt(config.port),
uuid: config.id,
alterId: parseInt(config.aid || 0),
cipher: config.scy || "auto",
tls: config.tls === "tls",
network: config.net || "tcp",
};
} catch {
return null;
}
}

function getCleanName(name) {
if (!name) return "unnamed";
return name.split("@").pop().split(":")[0].split(".")[0];
}

// --- Clash / Mihomo format ---

function formatTailscaleClash(data) {
const groups = [];
if (data.ipv4.length)
groups.push(data.ipv4.map((ip) => ` - IP-CIDR,${ip}/32,DIRECT,no-resolve`).join("\n"));
if (data.ipv6.length)
groups.push(data.ipv6.map((ip) => ` - IP-CIDR6,${ip}/128,DIRECT,no-resolve`).join("\n"));
return groups.join("\n\n");
}

async function generateClashYaml(proxies) {
const tailscaleData = await fetchTailscaleData();
const tailscaleRules = formatTailscaleClash(tailscaleData);

const proxiesList = proxies
.map((p) => {
if (p.protocol === PROTOCOL_SS_PREFIX) {
return ` - name: "${p.name}"
type: ss
server: ${p.server}
port: ${p.port}
cipher: ${p.cipher}
password: "${p.password}"
udp: true`;
} else if (p.protocol === PROTOCOL_VMESS_PREFIX) {
return ` - name: "${p.name}"
type: vmess
server: ${p.server}
port: ${p.port}
uuid: ${p.uuid}
alterId: ${p.alterId}
cipher: ${p.cipher}
tls: ${p.tls}
network: ${p.network}`;
}
return null;
})
.filter(Boolean)
.join("\n\n");

const proxyNames = proxies.map((p) => ` - "${p.name}"`).join("\n");
const udpProxyNames = proxies
.filter((p) => p.protocol === PROTOCOL_SS_PREFIX)
.map((p) => ` - "${p.name}"`)
.join("\n");

const tailscaleRulesBlock = tailscaleRules ? `${tailscaleRules}\n` : "";

return `mixed-port: 7890
allow-lan: true
mode: rule
bind-address: "*"
ipv6: false
find-process-mode: off
unified-delay: true
log-level: error
external-controller: 127.0.0.1:9090
routing-mark: 6666

geodata-mode: true
geodata-loader: standard
geo-auto-update: true
geo-update-interval: 24
geox-url:
geoip: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat"
geosite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb"
asn: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb"

tun:
enable: true
stack: system
dns-hijack:
- any:53
auto-route: true
route-exclude-address:
- 192.168.0.0/16
- 198.41.0.0/16
mtu: 1500
gso: true
gso-max-size: 65536

dns:
enable: true
listen: 0.0.0.0:53
ipv6: false
cache-algorithm: arc
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
fake-ip-filter-mode: blacklist
fake-ip-filter:
- "*"
- "+.lan"
- "+.local"
- "+.home.arpa"
- "+.in-addr.arpa"
- "+.ip6.arpa"
- "+.ts.net"
- "time.*.com"
- "ntp.*.com"
- "+.market.xiaomi.com"
- "geosite:private"
use-hosts: false
use-system-hosts: false
respect-rules: false
default-nameserver:
- tls://223.5.5.5
- tls://119.29.29.29
nameserver:
- https://dns.alidns.com/dns-query
- "https://dns.google/dns-query#PROXY"
proxy-server-nameserver:
- https://1.1.1.1/dns-query
- https://8.8.8.8/dns-query
- https://dns.alidns.com/dns-query
direct-nameserver:
- system
fallback:
- "https://1.1.1.1/dns-query#PROXY"
- "https://dns.google/dns-query#PROXY"
fallback-filter:
geoip: true
geoip-code: CN
ipcidr:
- 240.0.0.0/4
- 0.0.0.0/32
domain:
- "+.google.com"
- "+.facebook.com"
- "+.youtube.com"
nameserver-policy:
"geosite:cn":
- "https://dns.alidns.com/dns-query"
- "https://doh.pub/dns-query"
"geosite:gfw":
- "https://cloudflare-dns.com/dns-query#PROXY&disable-qtype-65=true"
- "https://dns.google/dns-query#PROXY&disable-qtype-65=true"
"+.arpa":
- system
"+.ts.net":
- "100.100.100.100#utun5"
"geosite:category-ads-all":
- "rcode://name_error"

proxies:
${proxiesList}

proxy-groups:
- name: PROXY
type: url-test
url: http://www.gstatic.com/generate_204
interval: 180
proxies:
${proxyNames}

- name: PROXY-UDP
type: url-test
url: http://www.gstatic.com/generate_204
interval: 180
proxies:
${udpProxyNames}

rules:
- GEOIP,lan,DIRECT,no-resolve
- GEOSITE,category-ads-all,REJECT
- GEOSITE,telegram,PROXY
- GEOIP,telegram,PROXY
- DOMAIN-SUFFIX,ts.net,DIRECT
- DOMAIN-WILDCARD,derp*.tailscale.com,DIRECT
- IP-CIDR,100.64.0.0/10,DIRECT
${tailscaleRulesBlock}
- GEOSITE,CN,DIRECT
- GEOIP,CN,DIRECT
- NETWORK,UDP,PROXY-UDP
- MATCH,PROXY
`;
}

附录 B:占位符对照表(按你的实际值替换)

占位符 含义
sub.example.com 你的自定义订阅域名
example.com 已托管在 Cloudflare 的 zone
YOUR_TOKEN 访问 Token(随机生成)
<YOUR_KV_NAMESPACE_ID> / NSID wrangler kv namespace create 输出的 KV id
jmssub.net/...service=XXXXXXX&id=xxxx... 你的机场订阅地址
jms-sub Worker 名(随意)