基于Cloudflare Worker的IPv4 to IPv6内网服务代理方案总结

本文最后更新于 2025年10月30日 上午

基于Cloudflare Worker的IPv4 to IPv6内网服务代理方案总结

背景与问题

用户需要在IPv4网络环境下访问家庭内网中仅支持IPv6的主机(6.xinhaojin.top)上的服务。由于IPv4网络无法直接访问IPv6-only主机,需要通过Cloudflare Worker搭建代理服务,实现:

  • IPv4 ↔ IPv6 网络协议转换
  • 单一域名支持多端口服务代理
  • 页面内资源请求正确转发

首先,新建一个worker绑定域名4.xinhaojin.top

方案一:路径前缀方案

核心思路

使用根域名 4.xinhaojin.top 配合路径前缀来区分不同端口服务:

1
2
https://4.xinhaojin.top/911/          → http://6.xinhaojin.top:911/
https://4.xinhaojin.top/912/admin → http://6.xinhaojin.top:912/admin

遇到的技术问题及解决方案

1. 静态资源路径丢失端口信息

问题:CSS、JS等静态文件请求如 /static/js/app.js 不包含端口号
解决方案

  • 从Referer头中提取端口号:referer.match(/\/\/(?:\d+\.)?4\.xinhaojin\.top\/(\d+)\//)
  • 使用Cookie持久化端口信息:XPORT cookie
  • 提供清晰的错误提示,引导用户使用正确格式

2. AJAX请求使用原始路径

问题:JavaScript发起的fetch/XHR请求未自动添加端口前缀
解决方案

  • 重写全局fetch API
  • 拦截XMLHttpRequest的open方法
  • 使用MutationObserver监控DOM变化
  • 拦截动态创建的元素属性设置

3. 后端重定向问题

问题:后端服务返回的重定向Location指向原始地址
解决方案

  • 重写Location响应头
  • 将后端重定向转换为代理格式

配置步骤

  1. DNS配置:确保 4.xinhaojin.top 已正确解析到Cloudflare
  2. Worker部署:部署路径前缀方案的Worker代码
  3. 路由设置:配置路由 4.xinhaojin.top/*

Worker代码核心逻辑

1
2
3
4
5
6
7
8
9
10
11
// 端口提取
const match = pathname.match(/^\/(\d+)(?:\/(.*))?$/);

// 从Referer恢复端口
const refererMatch = referer.match(/\/\/(?:\d+\.)?4\.xinhaojin\.top\/(\d+)\//);

// 内容重写
html.replace(/(href|src|action)=["'](\/[^"']*)["']/gi, ...);

// 客户端脚本注入
const clientScript = `...拦截fetch、XHR、动态元素的代码...`;

完整代码

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
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
addEventListener("fetch", event => {
event.respondWith(handleRequest(event.request));
});

const ORIGIN_HOST = "6.xinhaojin.top";

async function handleRequest(request) {
const url = new URL(request.url);
const pathname = url.pathname;

// 支持根路径显示使用说明
if (pathname === '/') {
return new Response(
`🚀 IPv6 代理服务\n\n` +
`使用方法:https://4.xinhaojin.top/<端口>/<路径>\n\n` +
`示例:\n` +
`https://4.xinhaojin.top/911/ → http://6.xinhaojin.top:911/\n` +
`https://4.xinhaojin.top/912/admin → http://6.xinhaojin.top:912/admin\n` +
`https://4.xinhaojin.top/8080/static/ → http://6.xinhaojin.top:8080/static/\n\n` +
`当前支持任意端口号访问 IPv6-only 服务`,
{
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-cache'
}
}
);
}

// 情况1:路径包含端口号 /911/static/js/app.js
let match = pathname.match(/^\/(\d+)(?:\/(.*))?$/);
let port = null;
let restPath = '';

if (match) {
port = match[1];
restPath = match[2] || '';
}
// 情况2:静态文件路径没有端口号 /static/js/app.js
else {
// 尝试从 Referer 头中提取端口号
const referer = request.headers.get('referer');
if (referer) {
const refererMatch = referer.match(/\/\/(?:\d+\.)?4\.xinhaojin\.top\/(\d+)\//);
if (refererMatch) {
port = refererMatch[1];
restPath = pathname.slice(1); // 去掉开头的斜杠
console.log(`Recovered port ${port} from Referer for path: ${pathname}`);
}
}

// 如果还是没有端口号,尝试从 Cookie 中获取
if (!port) {
const cookieHeader = request.headers.get('cookie');
if (cookieHeader) {
const cookieMatch = cookieHeader.match(/XPORT=(\d+)/);
if (cookieMatch) {
port = cookieMatch[1];
restPath = pathname.slice(1);
console.log(`Recovered port ${port} from Cookie for path: ${pathname}`);
}
}
}

// 如果仍然没有端口号,返回错误
if (!port) {
return new Response(
`❌ 无法确定目标端口\n\n` +
`请求路径:${pathname}\n` +
`此路径不包含端口号,且无法从引用页面确定目标端口。\n\n` +
`请确保:\n` +
`1. 从包含端口号的页面链接访问资源\n` +
`2. 或者使用完整路径:/<端口>${pathname}\n\n` +
`示例:\n` +
`https://4.xinhaojin.top/911${pathname}`,
{
status: 400,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
}
);
}
}

const target = `http://${ORIGIN_HOST}:${port}/${restPath}${url.search}`;

console.log(`Proxying: ${url.pathname} -> ${target} (port: ${port})`);

const reqHeaders = new Headers(request.headers);
reqHeaders.set("Host", ORIGIN_HOST);
reqHeaders.set("X-Forwarded-Host", url.hostname);
reqHeaders.set("X-Forwarded-Proto", "https");

// 移除可能干扰的头部
reqHeaders.delete("cf-connecting-ip");
reqHeaders.delete("cf-ipcountry");
reqHeaders.delete("cf-ray");
reqHeaders.delete("cf-visitor");

const fetchInit = {
method: request.method,
headers: reqHeaders,
redirect: "manual",
body: ["GET", "HEAD"].includes(request.method) ? undefined : request.body
};

let res;
try {
res = await fetch(target, fetchInit);
} catch (e) {
return new Response(
`❌ 上游服务连接失败\n\n` +
`错误信息:${e.message}\n` +
`目标地址:${target}\n` +
`恢复的端口:${port}\n\n` +
`可能的原因:\n` +
`• 端口 ${port} 没有服务在运行\n` +
`• 目标服务器 ${ORIGIN_HOST} 无法访问\n` +
`• 网络连接问题`,
{
status: 502,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
}
);
}

// 处理响应头
const newHeaders = new Headers();
for (const [k, v] of res.headers) {
if (k.toLowerCase() === "location" && v) {
try {
const locationUrl = new URL(v, target);
// 如果重定向到同域服务,改为我们的路径格式
if (locationUrl.hostname === ORIGIN_HOST) {
const redirectPort = locationUrl.port || '80';
const newLocation = `/${redirectPort}${locationUrl.pathname}${locationUrl.search}`;
newHeaders.set("Location", newLocation);
continue;
}
} catch (e) {
// URL 解析失败,保持原样
}
}

// 处理 Set-Cookie - 设置 Cookie 记住端口
if (k.toLowerCase() === "set-cookie") {
let modifiedCookie = v;
// 确保 cookie 在正确路径下
if (!/Path=\/.*\//i.test(modifiedCookie)) {
modifiedCookie = modifiedCookie.replace(/; Path=[^;]*/i, `; Path=/`);
}
newHeaders.append("Set-Cookie", modifiedCookie);
continue;
}

const hopByHop = new Set([
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
"te", "trailers", "transfer-encoding", "upgrade"
]);
if (!hopByHop.has(k.toLowerCase())) {
newHeaders.set(k, v);
}
}

// 设置端口 Cookie,便于后续请求恢复端口信息
newHeaders.append("Set-Cookie", `XPORT=${port}; Path=/; Max-Age=3600; SameSite=Lax`);

const contentType = res.headers.get("content-type") || "";

// 处理 HTML 内容重写
if (contentType.includes("text/html")) {
let html = await res.text();

// 重写 HTML 中的绝对路径
html = html.replace(
/(href|src|action)=["'](\/[^"']*)["']/gi,
(match, attr, path) => {
// 跳过已经包含端口前缀的路径
if (path.match(/^\/\d+\//) ||
path.startsWith('//') ||
path.startsWith('http://') ||
path.startsWith('https://')) {
return match;
}
return `${attr}="/${port}${path}"`;
}
);

// 重写 CSS 中的 URL
html = html.replace(
/url\(["']?(\/[^"')]*)["']?\)/gi,
(match, urlPath) => {
if (urlPath.match(/^\/\d+\//) ||
urlPath.startsWith('//') ||
urlPath.startsWith('http://') ||
urlPath.startsWith('https://')) {
return match;
}
return `url("/${port}${urlPath}")`;
}
);

// 注入增强的客户端重写脚本
const clientScript = `
<script>
// 增强的客户端路径重写
(function() {
const currentPort = "${port}";
const basePath = "/" + currentPort;

console.log('Path Rewrite Agent loaded for port:', currentPort);

// 设置文档基础URI
const baseElement = document.createElement('base');
baseElement.href = basePath + '/';
document.head.appendChild(baseElement);

// 重写路径函数
function rewritePath(url) {
if (!url || typeof url !== 'string') return url;

// 跳过已重写的URL和外部URL
if (url.startsWith(basePath) ||
url.startsWith('//') ||
url.startsWith('http://') ||
url.startsWith('https://') ||
!url.startsWith('/')) {
return url;
}

const newUrl = basePath + url;
console.log('Rewriting path:', url, '->', newUrl);
return newUrl;
}

// 重写现有DOM元素
function rewriteExistingElements() {
console.log('Rewriting existing DOM elements...');

// 重写所有链接
document.querySelectorAll('a[href]').forEach(link => {
const href = link.getAttribute('href');
if (href && href.startsWith('/') && !href.startsWith(basePath)) {
link.setAttribute('href', rewritePath(href));
}
});

// 重写所有表单
document.querySelectorAll('form[action]').forEach(form => {
const action = form.getAttribute('action');
if (action && action.startsWith('/') && !action.startsWith(basePath)) {
form.setAttribute('action', rewritePath(action));
}
});

// 重写所有资源
const resourceSelectors = [
'img[src]', 'script[src]', 'link[href]',
'iframe[src]', 'embed[src]', 'object[data]',
'source[src]', 'track[src]', 'audio[src]', 'video[src]'
];

resourceSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(el => {
const src = el.getAttribute('src');
const href = el.getAttribute('href');
const data = el.getAttribute('data');

if (src && src.startsWith('/') && !src.startsWith(basePath)) {
el.setAttribute('src', rewritePath(src));
}
if (href && el.tagName.toLowerCase() === 'link' && href.startsWith('/') && !href.startsWith(basePath)) {
el.setAttribute('href', rewritePath(href));
}
if (data && data.startsWith('/') && !data.startsWith(basePath)) {
el.setAttribute('data', rewritePath(data));
}
});
});
}

// 拦截fetch请求
const originalFetch = window.fetch;
window.fetch = function(...args) {
if (typeof args[0] === 'string') {
const newUrl = rewritePath(args[0]);
if (newUrl !== args[0]) {
console.log('Intercepted fetch:', args[0], '->', newUrl);
args[0] = newUrl;
}
} else if (args[0] instanceof Request) {
const newUrl = rewritePath(args[0].url);
if (newUrl !== args[0].url) {
console.log('Intercepted Request:', args[0].url, '->', newUrl);
args[0] = new Request(newUrl, args[0]);
}
}
return originalFetch.apply(this, args);
};

// 拦截XMLHttpRequest
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
if (typeof url === 'string') {
const newUrl = rewritePath(url);
if (newUrl !== url) {
console.log('Intercepted XHR:', url, '->', newUrl);
url = newUrl;
}
}
return originalOpen.call(this, method, url, ...rest);
};

// 拦截动态创建的元素
const originalCreateElement = document.createElement;
document.createElement = function(tagName) {
const element = originalCreateElement.call(this, tagName);

const rewriteAttributes = ['src', 'href', 'action', 'data'];

const originalSetAttribute = element.setAttribute;
element.setAttribute = function(name, value) {
if (rewriteAttributes.includes(name) && value && value.startsWith('/') && !value.startsWith(basePath)) {
value = rewritePath(value);
}
return originalSetAttribute.call(this, name, value);
};

return element;
};

// 监听DOM变化
const observer = new MutationObserver(function(mutations) {
let shouldRewrite = false;
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length) {
shouldRewrite = true;
}
});
if (shouldRewrite) {
setTimeout(rewriteExistingElements, 100);
}
});

observer.observe(document.body, {
childList: true,
subtree: true
});

// 初始重写
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(rewriteExistingElements, 100);
});
} else {
setTimeout(rewriteExistingElements, 100);
}
})();
</script>
`;

// 注入客户端脚本
if (html.includes('</head>')) {
html = html.replace('</head>', clientScript + '</head>');
} else if (html.includes('<head>')) {
html = html.replace('<head>', '<head>' + clientScript);
} else {
html = clientScript + html;
}

const bodyBytes = new TextEncoder().encode(html);
newHeaders.set("content-length", String(bodyBytes.length));

return new Response(bodyBytes, {
status: res.status,
statusText: res.statusText,
headers: newHeaders
});
}

// 对于静态文件(JS、CSS等),直接返回
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders
});
}

方案二:子域名方案

核心思路

使用子域名来区分不同端口服务:

1
2
http://911.4.xinhaojin.top/     → http://6.xinhaojin.top:911/
http://912.4.xinhaojin.top/admin → http://6.xinhaojin.top:912/admin

遇到的技术问题及解决方案

1. 三级子域名SSL证书问题

问题:Cloudflare免费账户不为三级子域名自动签发SSL证书
解决方案

  • 强制使用HTTP协议访问
  • 自动重定向HTTPS请求到HTTP
  • 移除Cookie的Secure标志

2. 页面内HTTPS资源请求失败

问题:页面内资源链接仍使用HTTPS协议
解决方案

  • 重写HTML中的HTTPS链接为HTTP
  • 重写JS/CSS文件中的协议相关URL
  • 设置正确的base标签

3. 后端链接重写

问题:后端返回的内容中包含原始HTTP链接
解决方案

  • 重写后端HTTP链接到代理子域名
  • 处理协议相对URL(//host/path)

配置步骤

  1. DNS配置:添加通配符CNAME记录 *.4.xinhaojin.top,CNAME记录,名称*.4,内容4.xinhaojin.top,开启代理
  2. Worker部署:部署子域名方案的Worker代码
  3. 路由设置:配置workers路由 *.4.xinhaojin.top/*
  4. SSL设置:SSL/TLS加密模式设置为”关闭”

Worker代码核心逻辑

1
2
3
4
5
6
7
8
9
10
11
12
// 强制HTTP访问
if (url.protocol === 'https:') {
url.protocol = 'http:';
return Response.redirect(url.toString(), 301);
}

// 子域名端口提取
const portMatch = hostname.match(/^(\d+)\.4\.xinhaojin\.top$/);

// 协议重写
html.replace(new RegExp(`https://${currentSubdomain}`, 'g'),
`http://${currentSubdomain}`);

完整代码

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
addEventListener("fetch", event => {
event.respondWith(handleRequest(event.request));
});

const ORIGIN_HOST = "6.xinhaojin.top";

async function handleRequest(request) {
const url = new URL(request.url);
const hostname = url.hostname;

// 如果是 HTTPS 请求,重定向到 HTTP
if (url.protocol === 'https:') {
url.protocol = 'http:';
return Response.redirect(url.toString(), 301);
}

console.log(`Received request for: ${hostname}${url.pathname}`);

// 从子域名提取端口:911.4.xinhaojin.top -> 911
const portMatch = hostname.match(/^(\d+)\.4\.xinhaojin\.top$/);

if (!portMatch) {
// 如果不是端口格式的子域名,返回使用说明
return new Response(
`🚀 IPv6 代理服务\n\n` +
`使用方法:\n` +
` http://<端口>.4.xinhaojin.top/<路径>\n\n` +
`示例:\n` +
` http://911.4.xinhaojin.top/ → http://6.xinhaojin.top:911/\n` +
` http://912.4.xinhaojin.top/admin → http://6.xinhaojin.top:912/admin\n\n` +
`当前请求:${request.url}\n` +
`注意:请使用 HTTP 协议访问`,
{
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-cache'
}
}
);
}

const port = portMatch[1];
const target = `http://${ORIGIN_HOST}:${port}${url.pathname}${url.search}`;

console.log(`Proxying: ${request.url} -> ${target}`);

const reqHeaders = new Headers(request.headers);

// 重要:设置正确的 Host 头
reqHeaders.set("Host", `${ORIGIN_HOST}:${port}`);

// 移除 Cloudflare 特定的头部,避免干扰后端
reqHeaders.delete("cf-connecting-ip");
reqHeaders.delete("cf-ipcountry");
reqHeaders.delete("cf-ray");
reqHeaders.delete("cf-visitor");

// 添加 X-Forwarded-* 头部,让后端知道真实情况
reqHeaders.set("X-Forwarded-Host", hostname);
reqHeaders.set("X-Forwarded-Proto", "http"); // 改为 http
reqHeaders.set("X-Forwarded-For", request.headers.get("cf-connecting-ip") || "");

const fetchInit = {
method: request.method,
headers: reqHeaders,
redirect: "manual",
body: ["GET", "HEAD"].includes(request.method) ? undefined : request.body
};

let res;
try {
res = await fetch(target, fetchInit);
} catch (e) {
// 更详细的错误处理
const errorMessage = e.message || "Unknown error";
console.error(`Fetch failed: ${errorMessage}, Target: ${target}`);

return new Response(
`❌ 上游服务连接失败\n\n` +
`错误信息:${errorMessage}\n` +
`目标地址:${target}\n\n` +
`可能的原因:\n` +
`• 端口 ${port} 没有服务在运行\n` +
`• 目标服务器 ${ORIGIN_HOST} 无法访问\n` +
`• 网络连接或防火墙问题\n` +
`• 后端服务只支持 IPv6 访问\n\n` +
`调试信息:\n` +
`• 请求协议: HTTP (后端服务)\n` +
`• 目标端口: ${port}\n` +
`• 目标主机: ${ORIGIN_HOST}`,
{
status: 502,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-cache'
}
}
);
}

// 处理响应头
const newHeaders = new Headers();

// 复制所有原始头部,除了需要修改的
for (const [k, v] of res.headers) {
// 处理重定向 Location 头
if (k.toLowerCase() === "location" && v) {
try {
const locationUrl = new URL(v, target);

// 如果重定向到同域 HTTP 服务,改为我们的 HTTP 子域名格式
if (locationUrl.hostname === ORIGIN_HOST) {
const redirectPort = locationUrl.port || '80';
const newLocation = `http://${redirectPort}.4.xinhaojin.top${locationUrl.pathname}${locationUrl.search}`;
newHeaders.set("Location", newLocation);
continue;
}
} catch (e) {
// URL 解析失败,保持原样
console.warn(`Failed to parse Location header: ${v}`, e);
}
}

// 处理 Set-Cookie:移除 Secure 标志,因为我们使用 HTTP
if (k.toLowerCase() === "set-cookie") {
let modifiedCookie = v;
// 如果 cookie 没有指定 Domain,或者 Domain 是后端域名,进行调整
if (!/Domain=/i.test(v) || new RegExp(`Domain=${ORIGIN_HOST}`, 'i').test(v)) {
modifiedCookie = v.replace(
/(?:;\s*Domain=)[^;]*/i,
`; Domain=.4.xinhaojin.top`
);
}
// 移除 Secure 标志(因为我们使用 HTTP)
modifiedCookie = modifiedCookie.replace(/;\s*Secure/gi, '');
newHeaders.append("Set-Cookie", modifiedCookie);
continue;
}

// 跳过 hop-by-hop 头部
const hopByHop = new Set([
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
"te", "trailers", "transfer-encoding", "upgrade"
]);
if (hopByHop.has(k.toLowerCase())) continue;

newHeaders.set(k, v);
}

// 添加安全相关的头部
newHeaders.set("X-Content-Type-Options", "nosniff");
newHeaders.set("X-Frame-Options", "SAMEORIGIN");

// 对于 HTML 内容,重写其中的 HTTPS 链接为 HTTP
const contentType = res.headers.get("content-type") || "";
if (contentType.includes("text/html")) {
let html = await res.text();

const currentSubdomain = hostname; // 如 911.4.xinhaojin.top

// 将所有 HTTPS 链接改为 HTTP
html = html.replace(
new RegExp(`https://${currentSubdomain}`, 'g'),
`http://${currentSubdomain}`
);

// 重写后端 HTTP 链接到我们的 HTTP 代理
html = html.replace(
new RegExp(`http://${ORIGIN_HOST}:${port}`, 'g'),
`http://${currentSubdomain}`
);

// 重写相对路径的协议相对URL(//host/path)
html = html.replace(
new RegExp(`//${ORIGIN_HOST}:${port}`, 'g'),
`//${currentSubdomain}`
);

// 注入基础标签确保相对路径正确
const baseTag = `<base href="http://${currentSubdomain}/">`;
if (html.includes('</head>')) {
html = html.replace('</head>', baseTag + '</head>');
} else if (html.includes('<head>')) {
html = html.replace('<head>', '<head>' + baseTag);
} else {
html = baseTag + html;
}

const bodyBytes = new TextEncoder().encode(html);
newHeaders.set("content-length", String(bodyBytes.length));

return new Response(bodyBytes, {
status: res.status,
statusText: res.statusText,
headers: newHeaders
});
}

// 对于 JavaScript 和 CSS,重写其中的 HTTPS 链接
if (contentType.includes("application/javascript") ||
contentType.includes("text/css")) {
let content = await res.text();
const currentSubdomain = hostname;

// 重写 JS/CSS 中的 HTTPS 链接为 HTTP
content = content.replace(
new RegExp(`https://${currentSubdomain}`, 'g'),
`http://${currentSubdomain}`
);

// 重写 JS/CSS 中的后端 URL
content = content.replace(
new RegExp(`http://${ORIGIN_HOST}:${port}`, 'g'),
`http://${currentSubdomain}`
);

const bodyBytes = new TextEncoder().encode(content);
newHeaders.set("content-length", String(bodyBytes.length));

return new Response(bodyBytes, {
status: res.status,
statusText: res.statusText,
headers: newHeaders
});
}

// 其他类型内容直接返回
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders
});
}

方案对比与总结

路径前缀方案

优点:

  • ✅ 完整的HTTPS支持,安全性高
  • ✅ 无需额外DNS配置
  • ✅ 浏览器无安全警告
  • ✅ 兼容性最好

缺点:

  • ❌ URL不够简洁美观
  • ❌ 需要复杂的客户端重写逻辑
  • ❌ 维护成本较高

子域名方案

优点:

  • ✅ URL简洁直观,易于记忆
  • ✅ 无需路径重写,逻辑简单
  • ✅ 页面内请求自然正确
  • ✅ 维护简单

缺点:

  • ❌ 只能使用HTTP协议
  • ❌ 浏览器显示”不安全”警告
  • ❌ 需要通配符DNS配置
  • ❌ 某些网络环境可能阻止HTTP访问

推荐使用场景

路径前缀方案适用于:

  • 生产环境对外服务
  • 需要HTTPS安全性的场景
  • 对URL美观度要求不高的内部系统

子域名方案适用于:

  • 开发和测试环境
  • 内部网络使用
  • 对URL简洁性有要求的场景

两种方案都经过充分测试,基本能够解决IPv4访问IPv6内网服务的核心需求,但不是访问内网服务的首选方案,仅作为临时应急备用,首选应当还是在有IPv6的环境下直接使用服务器的公网IPv6。


基于Cloudflare Worker的IPv4 to IPv6内网服务代理方案总结
https://xinhaojin.github.io/2025/10/30/基于Cloudflare Worker的IPv4 to IPv6内网服务代理方案总结/
作者
xinhaojin
发布于
2025年10月30日
许可协议