上个月的时候由于 Cloudflare 更新了用户协议,明确禁止了使用 Cloudflare 的进行代理服务,导致我之前一直使用的双域名优选 IP 的方案不好再继续使用。
想了想还是得从客户端入手,毕竟用户改 IP 关我站长什么事。
原先我自己其实也有本地的优选 IP 方案,但是每次更新 IP 都要在本地执行一个脚本来刷新 hosts,比较麻烦。
于是想到使用 vless 指定 IP,然后再在 transport 中指定伪装域名的方式来实现。
说干就干,立马写了一个脚本来实现这个方案。
1type Carrier = "CM" | "CU" | "CT";
2
3interface IpInfo {
4 ip: string;
5 colo: string;
6 bandwidth: string;
7 speed: string;
8 latency: string;
9 updateTime: string;
10}
11
12interface IpGroups {
13 CM: IpInfo[];
14 CU: IpInfo[];
15 CT: IpInfo[];
16}
17
18interface LinkResult {
19 isp: Carrier;
20 url: string;
21 originalUrl: string;
22}
23
24export async function cf_ips(vlessUrl: string): Promise<string[]> {
25 const response = await fetch("http://www.wetest.vip/page/cloudflare/address_v4.html");
26 const html = await response.text();
27
28 // 解析HTML获取IP信息
29 const allIPs = parseIPsFromHTML(html);
30
31 if (Object.values(allIPs).every(arr => arr.length === 0)) {
32 throw new Error("无法获取 Cloudflare IP 信息");
33 }
34
35 // 处理单个 cf:// 链接
36 const results = processSingleLink(allIPs, vlessUrl);
37
38 // 提取所有处理后的链接,直接返回数组
39 return results.map(result => result.url.replace('cf://', ''));
40}
41
42function parseIPsFromHTML(html: string): IpGroups {
43 const allIPs: IpGroups = {
44 CM: [], // 移动
45 CU: [], // 联通
46 CT: [] // 电信
47 };
48
49 // 使用正则表达式匹配表格行
50 const rows: string[] = html.match(/<tr>[^]*?<\/tr>/g) || [];
51
52 for (const row of rows) {
53 // 跳过表头
54 if (row.includes("<th>")) continue;
55
56 // 提取单元格数据
57 const cells: string[] = row.match(/<td[^>]*data-label="[^"]*">([^<]+)<\/td>/g) || [];
58 if (cells.length >= 7) {
59 const carrier = (cells[0].match(/data-label="[^"]*">([^<]+)<\/td>/) as RegExpMatchArray)[1].trim();
60 const ip = (cells[1].match(/data-label="[^"]*">([^<]+)<\/td>/) as RegExpMatchArray)[1].trim();
61 const bandwidth = (cells[2].match(/data-label="[^"]*">([^<]+)<\/td>/) as RegExpMatchArray)[1].trim();
62 const speed = (cells[3].match(/data-label="[^"]*">([^<]+)<\/td>/) as RegExpMatchArray)[1].trim();
63 const latency = (cells[4].match(/data-label="[^"]*">([^<]+)<\/td>/) as RegExpMatchArray)[1].trim();
64 const colo = (cells[5].match(/data-label="[^"]*">([^<]+)<\/td>/) as RegExpMatchArray)[1].trim();
65 const updateTime = (cells[6].match(/data-label="[^"]*">([^<]+)<\/td>/) as RegExpMatchArray)[1].trim();
66
67 const ipInfo: IpInfo = {
68 ip,
69 colo,
70 bandwidth,
71 speed,
72 latency,
73 updateTime
74 };
75
76 // 根据运营商分类
77 switch (carrier) {
78 case "移动":
79 allIPs.CM.push(ipInfo);
80 break;
81 case "联通":
82 allIPs.CU.push(ipInfo);
83 break;
84 case "电信":
85 allIPs.CT.push(ipInfo);
86 break;
87 }
88 }
89 }
90
91 return allIPs;
92}
93
94function processSingleLink(allIPs: IpGroups, vlessUrl: string): LinkResult[] {
95 // 检查是否是cf://开头的链接
96 if (!vlessUrl.startsWith('cf://vless://')) {
97 return [];
98 }
99
100 const vlessUrlContent = vlessUrl.substring(5);
101
102 const match = vlessUrlContent.match(/@([^:]+):/);
103 if (!match) return [];
104
105 const originalDomain = match[1];
106
107 const results: LinkResult[] = [];
108
109 // 遍历每个运营商的所有 IP
110 for (const [isp, ipList] of Object.entries(allIPs) as [Carrier, IpInfo[]][]) {
111 for (const ipInfo of ipList) {
112 const newUrl = vlessUrlContent.replace(
113 `@${originalDomain}:`,
114 `@${ipInfo.ip}:`
115 );
116
117 let modifiedUrl = newUrl;
118 const ispLabel = `${isp}-${ipInfo.colo}`;
119
120 // 在 type 参数后添加 host 参数
121 const typeIndex = modifiedUrl.indexOf('type=');
122 if (typeIndex !== -1) {
123 const afterType = modifiedUrl.indexOf('&', typeIndex);
124 if (afterType !== -1) {
125 modifiedUrl = modifiedUrl.slice(0, afterType) +
126 `&host=${originalDomain}` +
127 modifiedUrl.slice(afterType);
128 } else {
129 modifiedUrl = modifiedUrl + `&host=${originalDomain}`;
130 }
131 } else if (modifiedUrl.includes('?')) {
132 modifiedUrl = modifiedUrl + `&host=${originalDomain}`;
133 } else {
134 modifiedUrl = modifiedUrl + `?host=${originalDomain}`;
135 }
136
137 // 处理备注部分
138 const hashIndex = modifiedUrl.indexOf('#');
139 if (hashIndex !== -1) {
140 modifiedUrl = modifiedUrl.slice(0, hashIndex + 1) +
141 ispLabel +
142 modifiedUrl.slice(hashIndex + 1);
143 } else {
144 modifiedUrl = modifiedUrl + '#' + ispLabel;
145 }
146
147 results.push({
148 isp,
149 url: `cf://${modifiedUrl}`,
150 originalUrl: vlessUrl
151 });
152 }
153 }
154
155 return results;
156}
简单说明一下这个脚本的实现逻辑,通过
https://www.wetest.vip/
提供的「Cloudflare优选IP」服务,获取到 Cloudflare 的优选 IP 信息,然后通过脚本解析传入的 vless 分享链接列表中以 cf:// 开头的链接,并按格式用优选 IP 替换掉 vless 中的 IP 地址,然后返回新的 vless 链接列表。实现了这个功能以后已经可以在 Clash 中使用优选 IP 了,但是我这边又遇到问题,在 Clash 中一旦触发了延迟测试,会向所有的节点发送请求,导致 Cloudflare 不再转发我的请求。(暂不知道是 Cloudflare 还是我这边运营商的问题)
为了解决这个问题,我将优选 IP 的节点从中随机选择一个返回,然后设置 Clash 自动更新,这样每次就会随机选择一个返回。
为了方便这些脚本的组织与执行,我使用的 Cloudflare Worker。有需要的可以参考我的 Cloudflare Worker Sub 仓库。
源仓库 fork 自 cmliu,感谢大佬的贡献。