Skip to content

Commit a01ef01

Browse files
committed
Merge branch 'main' of https://github.com/cmliu/edgetunnel
2 parents 29cbc5c + 92fc6cc commit a01ef01

2 files changed

Lines changed: 157 additions & 69 deletions

File tree

CHANGELOG

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## [2.1.20260811144522] - 2026-08-11 14:45:22
2+
3+
### Change
4+
5+
- 优化 **XHTTP** 上行传输:新增数据合包机制,小块数据合并后再统一发送,减少小包频繁写入,降低资源开销,大文件上传更流畅。
6+
17
## [2.1.20260810030901] - 2026-08-10 03:09:01
28

39
### ADD

_worker.js

Lines changed: 151 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const Version = '2026-08-10 03:09:01';
1+
const Version = '2026-08-11 14:45:22';
22
let config_JSON, 缓存SOCKS5白名单 = null, 调试日志打印 = false;
33
let SOCKS5白名单 = ['*tapecontent.net', '*cloudatacdn.com', '*loadshare.org', '*cdn-centaurus.com', 'scholar.google.com'];
44
const Pages静态页面 = 'https://edt-pages.github.io';
@@ -68,19 +68,16 @@ export default {
6868
const 反代上下文 = await 反代参数获取(url, userID, 默认反代IP, 默认反代兜底);
6969
log(`[WebSocket] 命中请求: ${url.pathname}${url.search}`);
7070
return await 处理WS请求(request, userID, url, 反代上下文);
71-
} else if (管理员密码 && !访问路径.startsWith('admin/') && 访问路径 !== 'login' && request.method === 'POST') {// gRPC/XHTTP代理
71+
} else if (管理员密码 && !访问路径.startsWith('admin/') && 访问路径 !== 'login' && request.method === 'POST') {// gRPC/叉HTTP代理
7272
const 反代上下文 = await 反代参数获取(url, userID, 默认反代IP, 默认反代兜底);
73-
const referer = request.headers.get('Referer') || '';
74-
const { : 本机Padding头, : 本机Padding键 } = 获取XHTTPPadding标识(userID);
75-
const 命中XHTTP特征 = referer.includes('x_padding', 14) || referer.includes('x_padding=')
76-
|| !!request.headers.get(本机Padding头)
77-
|| !!url.searchParams.get(本机Padding键);
78-
if (!命中XHTTP特征 && contentType.startsWith('application/grpc')) {
73+
const { : 本机Padding头, : 本机Padding键 } = 获取叉HTTPPadding标识(userID);
74+
const 命中叉HTTP特征 = !!request.headers.get(本机Padding头) || !!url.searchParams.get(本机Padding键);
75+
if (!命中叉HTTP特征 && contentType.startsWith('application/grpc')) {
7976
log(`[gRPC] 命中请求: ${url.pathname}${url.search}`);
8077
return await 处理gRPC请求(request, userID, 反代上下文);
8178
}
82-
log(`[XHTTP] 命中请求: ${url.pathname}${url.search}`);
83-
return await 处理XHTTP请求(request, userID, 反代上下文);
79+
log(`[叉HTTP] 命中请求: ${url.pathname}${url.search}`);
80+
return await 处理叉HTTP请求(request, userID, 反代上下文);
8481
} else {
8582
if (url.protocol === 'http:') return Response.redirect(url.href.replace(`http://${url.hostname}`, `https://${url.hostname}`), 301);
8683
if (!管理员密码) return fetch(Pages静态页面 + '/noADMIN').then(r => { const headers = new Headers(r.headers); headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); headers.set('Pragma', 'no-cache'); headers.set('Expires', '0'); return new Response(r.body, { status: 404, statusText: r.statusText, headers }) });
@@ -530,14 +527,7 @@ export default {
530527
return new Response(await nginx(), { status: 200, headers: { 'Content-Type': 'text/html; charset=UTF-8' } });
531528
}
532529
};
533-
///////////////////////////////////////////////////////////////////////XHTTP传输数据///////////////////////////////////////////////
534-
// ========== XHTTP obfs padding 支持 ==========
535-
// 链接生成处(获取传输协议配置)为 xhttp 节点注入:
536-
// extra={"xPaddingObfsMode":true,"xPaddingMethod":"tokenish","xPaddingPlacement":"queryInHeader",
537-
// "xPaddingHeader":"<UUID.slice(1,7)>","xPaddingKey":"_<UUID.slice(1,7)>"}
538-
// 客户端据此把 base62 padding 以「URL 形式放进 xPaddingHeader 头」或「放进 URL query」发送;
539-
// 服务端需提取并做 HPACK Huffman 字节长度校验(tokenish:100-2 <= len <= 1000+2),校验通过后 padding 直接丢弃。
540-
// 以下 HPACK Huffman 码长为 RFC 7541 Appendix B 的完整 257 项(含索引 256 的 EOS 符号),取自 golang.org/x/net/http2/hpack/tables.go,
530+
///////////////////////////////////////////////////////////////////////叉HTTP传输数据///////////////////////////////////////////////
541531
const HPACKHuffman码长 = [
542532
13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28,
543533
28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28,
@@ -558,12 +548,10 @@ const HPACKHuffman码长 = [
558548
30
559549
];
560550

561-
// 由 UUID 推导本机 XHTTP padding 头名/键名(与链接生成处 extra 的 xPaddingHeader / xPaddingKey 完全一致)
562-
function 获取XHTTPPadding标识(yourUUID) {
563-
return { : yourUUID.slice(1, 7), : '_' + yourUUID.slice(1, 7) };
551+
function 获取叉HTTPPadding标识(yourUUID) {
552+
return { : yourUUID.slice(1, 7), : '_' + yourUUID.slice(25, 31) };
564553
}
565554

566-
// 计算字符串经 HPACK Huffman 编码后的字节长度(向上取整;与官方 hpack.HuffmanEncodeLength 等价,按 UTF-8 字节遍历)
567555
function 计算HPACKHuffman字节长度(字符串) {
568556
const 字节 = new TextEncoder().encode(字符串);
569557
let 总位数 = 0;
@@ -573,10 +561,7 @@ function 计算HPACKHuffman字节长度(字符串) {
573561
return Math.ceil(总位数 / 8);
574562
}
575563

576-
// 提取 XHTTP obfs padding 值(对应官方 ExtractXPaddingFromRequest,obfs 模式;本项目客户端使用 queryInHeader,不启用 cookie)
577-
// 优先级:① Header xPaddingHeader(值为 URL 形式时取其中 query 参数 xPaddingKey,取不到则回退用头值本身)② URL query 参数 xPaddingKey
578-
// 返回 '' 表示请求未携带 padding
579-
function 提取XHTTPPadding值(request, 本机Padding头, 本机Padding键) {
564+
function 提取叉HTTPPadding值(request, 本机Padding头, 本机Padding键) {
580565
const 头值 = request.headers.get(本机Padding头);
581566
if (头值) {
582567
try {
@@ -590,35 +575,29 @@ function 提取XHTTPPadding值(request, 本机Padding头, 本机Padding键) {
590575
return 请求URL.searchParams.get(本机Padding键) || '';
591576
}
592577

593-
// 校验 XHTTP obfs padding(对应官方 IsPaddingValid,tokenish 方法)
594-
// @returns {boolean} true=校验通过或请求未携带 padding(放行);false=携带 padding 但校验失败(应返回 400)
595-
function 校验XHTTPPadding(request, 本机Padding头, 本机Padding键) {
596-
const padding值 = 提取XHTTPPadding值(request, 本机Padding头, 本机Padding键);
597-
if (!padding值) return true; // 无 padding:兼容旧客户端/非 padding 请求,直接放行
578+
function 校验叉HTTPPadding(request, 本机Padding头, 本机Padding键) {
579+
const padding值 = 提取叉HTTPPadding值(request, 本机Padding头, 本机Padding键);
580+
if (!padding值) return true;
598581
const huffman长度 = 计算HPACKHuffman字节长度(padding值);
599-
// tokenish:huffman 编码字节长度须在 [100-2, 1000+2] 容差范围内
600582
return huffman长度 >= 98 && huffman长度 <= 1002;
601583
}
602584

603-
// 生成随机 base62 padding 串(用于响应端 padding,官方 GeneratePadding tokenish 的简化版;客户端不校验响应 padding)
604-
const XHTTPBase62字符集 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
605-
function 生成XHTTPPadding串(长度) {
606-
const 字符集长度 = XHTTPBase62字符集.length;
585+
const 叉HTTPBase62字符集 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
586+
function 生成叉HTTPPadding串(长度) {
587+
const 字符集长度 = 叉HTTPBase62字符集.length;
607588
let 结果 = '';
608589
for (let i = 0; i < 长度; i++) {
609-
结果 += XHTTPBase62字符集[Math.floor(Math.random() * 字符集长度)];
590+
结果 += 叉HTTPBase62字符集[Math.floor(Math.random() * 字符集长度)];
610591
}
611592
return 结果;
612593
}
613594

614-
async function 处理XHTTP请求(request, yourUUID, 反代上下文 = {}) {
595+
async function 处理叉HTTP请求(request, yourUUID, 反代上下文 = {}) {
615596
if (!request.body) return new Response('Bad Request', { status: 400 });
616-
// XHTTP obfs padding 提取与校验(官方 ExtractXPaddingFromRequest + IsPaddingValid)
617-
// 校验通过或请求未携带 padding → 放行;携带 padding 但校验失败 → 400(padding 数据不解码、直接丢弃,不影响首包解析)
618-
const { : 本机Padding头, : 本机Padding键 } = 获取XHTTPPadding标识(yourUUID);
619-
if (!校验XHTTPPadding(request, 本机Padding头, 本机Padding键)) return new Response('Bad Request', { status: 400 });
597+
const { : 本机Padding头, : 本机Padding键 } = 获取叉HTTPPadding标识(yourUUID);
598+
if (!校验叉HTTPPadding(request, 本机Padding头, 本机Padding键)) return new Response('Bad Request', { status: 400 });
620599
const reader = request.body.getReader();
621-
const 首包 = await 读取XHTTP首包(reader, yourUUID);
600+
const 首包 = await 读取叉HTTP首包(reader, yourUUID);
622601
if (!首包) {
623602
try { reader.releaseLock() } catch (e) { }
624603
return new Response('Invalid request', { status: 400 });
@@ -644,18 +623,15 @@ async function 处理XHTTP请求(request, yourUUID, 反代上下文 = {}) {
644623
'X-Accel-Buffering': 'no',
645624
'Cache-Control': 'no-store'
646625
});
647-
// 响应端 padding(官方 ApplyXPaddingToResponse 对应,obfs queryInHeader:头名=本机Padding头,值为含 query 的 URL 形式)
648-
// 客户端不校验响应 padding,仅作响应特征混淆;随机长度 100~1000
626+
649627
try {
650628
const 响应URL = new URL('https://x.invalid/');
651-
响应URL.searchParams.set(本机Padding键, 生成XHTTPPadding串(100 + Math.floor(Math.random() * 901)));
629+
响应URL.searchParams.set(本机Padding键, 生成叉HTTPPadding串(100 + Math.floor(Math.random() * 901)));
652630
responseHeaders.set(本机Padding头, 响应URL.toString());
653631
} catch (e) { }
654632

655-
// UDP 分支:拆到独立函数(保留原逻辑)
656-
if (首包.isUDP) return 处理XHTTPUDP请求(首包, reader, request, 反代上下文, responseHeaders);
633+
if (首包.isUDP) return 处理叉HTTPUDP请求(首包, reader, request, 反代上下文, responseHeaders);
657634

658-
// ================= TCP 分支:pipe 对接 =================
659635
try { reader.releaseLock() } catch (e) { }
660636

661637
const remoteConnWrapper = { socket: null, connectingPromise: null, retryConnect: null, downlinkDrain: Promise.resolve() };
@@ -665,18 +641,16 @@ async function 处理XHTTP请求(request, yourUUID, 反代上下文 = {}) {
665641
if (已清理) return;
666642
已清理 = true;
667643
try { abortController.abort(reason) } catch (e) { }
668-
失效TCP连接世代(remoteConnWrapper); // 关闭 socket + 世代 +1
644+
失效TCP连接世代(remoteConnWrapper);
669645
};
670646

671-
// ⚠️ 关键:ws 参数必须传占位对象(新版 forwardataTCP 2370 行会访问 ws.readyState,传 null 会 TypeError 崩掉)
672-
// closeSocketQuietly 对占位对象安全(无 close 方法 → TypeError 被内部 catch 吞掉)
673647
const 占位WS = { readyState: WebSocket.OPEN };
674648

675649
let socket;
676650
try {
677651
socket = await forwardataTCP(首包.hostname, 首包.port, 首包.rawData, 占位WS, 首包.respHeader, remoteConnWrapper, yourUUID, request, 反代上下文, 首包.协议 === 'trojan', 首包.原始数据, true);
678652
} catch (err) {
679-
log(`[XHTTP-Pipe] 连接失败: ${err?.message || err}`);
653+
log(`[叉HTTP-Pipe] 连接失败: ${err?.message || err}`);
680654
清理(err);
681655
return new Response('bad gateway', { status: 502 });
682656
}
@@ -685,12 +659,32 @@ async function 处理XHTTP请求(request, yourUUID, 反代上下文 = {}) {
685659
return new Response('bad gateway', { status: 502 });
686660
}
687661

688-
// 上行:请求体直接 pipe 进 socket(首包残余数据已由 forwardataTCP 写入)
689662
const 上行Promise = (async () => {
690-
await request.body.pipeTo(socket.writable, { signal: abortController.signal });
663+
const 上行合包器 = 创建上行Grain合包流();
664+
const 搬运Promise = 上行合包器.readable.pipeTo(socket.writable, { signal: abortController.signal });
665+
void 搬运Promise.catch(清理);
666+
const 上行reader = request.body.getReader();
667+
const 取消上行reader = () => {
668+
try { 上行reader.cancel(abortController.signal.reason).catch(() => { }); } catch (e) { }
669+
};
670+
abortController.signal.addEventListener('abort', 取消上行reader, { once: true });
671+
try {
672+
try {
673+
while (true) {
674+
const { done, value } = await 上行reader.read();
675+
if (done) break;
676+
if (value?.byteLength) await 上行合包器.写入(value);
677+
}
678+
} finally {
679+
abortController.signal.removeEventListener('abort', 取消上行reader);
680+
try { 上行reader.releaseLock() } catch (e) { }
681+
}
682+
} finally {
683+
try { await 上行合包器.结束() } catch (e) { }
684+
}
685+
await 搬运Promise;
691686
})();
692687

693-
// 下行:优先使用 IdentityTransformStream(若运行时不支持则回退到 TransformStream)
694688
const 响应流 = typeof IdentityTransformStream !== 'undefined'
695689
? new IdentityTransformStream()
696690
: new TransformStream();
@@ -714,13 +708,13 @@ async function 处理XHTTP请求(request, yourUUID, 反代上下文 = {}) {
714708
return new Response(响应流.readable, { status: 200, headers: responseHeaders });
715709
}
716710

717-
function 处理XHTTPUDP请求(首包, reader, request, 反代上下文, responseHeaders) {
711+
function 处理叉HTTPUDP请求(首包, reader, request, 反代上下文, responseHeaders) {
718712
const 木马UDP上下文 = { 缓存: new Uint8Array(0), 反代地址: 反代上下文.木马反代地址 };
719713
return new Response(new ReadableStream({
720714
async start(controller) {
721715
let 已关闭 = false;
722716
let udpRespHeader = 首包.respHeader;
723-
const xhttpBridge = {
717+
const 叉桥 = {
724718
readyState: WebSocket.OPEN,
725719
send(data) {
726720
if (已关闭) return;
@@ -750,30 +744,30 @@ function 处理XHTTPUDP请求(首包, reader, request, 反代上下文, response
750744
if (首包.协议 === 'trojan') {
751745
木马UDP上下文.目标主机 = 首包.hostname;
752746
木马UDP上下文.目标端口 = 首包.port;
753-
if (木马UDP上下文.反代地址) await 转发木马UDP数据(首包.原始数据, xhttpBridge, 木马UDP上下文, request);
747+
if (木马UDP上下文.反代地址) await 转发木马UDP数据(首包.原始数据, 叉桥, 木马UDP上下文, request);
754748
}
755749
if (!(首包.协议 === 'trojan' && 木马UDP上下文.反代地址) && 首包.rawData?.byteLength) {
756-
if (首包.协议 === 'trojan') await 转发木马UDP数据(首包.rawData, xhttpBridge, 木马UDP上下文, request);
757-
else await forwardataudp(首包.rawData, xhttpBridge, udpRespHeader, request);
750+
if (首包.协议 === 'trojan') await 转发木马UDP数据(首包.rawData, 叉桥, 木马UDP上下文, request);
751+
else await forwardataudp(首包.rawData, 叉桥, udpRespHeader, request);
758752
udpRespHeader = null;
759753
}
760754
while (true) {
761755
const { done, value } = await reader.read();
762756
if (done) break;
763757
if (!value || value.byteLength === 0) continue;
764-
if (首包.协议 === 'trojan') await 转发木马UDP数据(value, xhttpBridge, 木马UDP上下文, request);
765-
else await forwardataudp(value, xhttpBridge, udpRespHeader, request);
758+
if (首包.协议 === 'trojan') await 转发木马UDP数据(value, 叉桥, 木马UDP上下文, request);
759+
else await forwardataudp(value, 叉桥, udpRespHeader, request);
766760
udpRespHeader = null;
767761
}
768762
} catch (err) {
769763
转发失败 = true;
770-
log(`[XHTTP转发] 处理失败: ${err?.message || err}`);
771-
closeSocketQuietly(xhttpBridge);
764+
log(`[叉HTTP转发] 处理失败: ${err?.message || err}`);
765+
closeSocketQuietly(叉桥);
772766
} finally {
773767
const 保持木马UDP反代下行 = !转发失败 && 首包.协议 === 'trojan' && 木马UDP上下文.反代地址 && 木马UDP上下文.反代Socket;
774768
if (!保持木马UDP反代下行) {
775769
try { 木马UDP上下文.反代Socket?.close() } catch (e) { }
776-
closeSocketQuietly(xhttpBridge);
770+
closeSocketQuietly(叉桥);
777771
}
778772
try { reader.releaseLock() } catch (e) { }
779773
}
@@ -821,7 +815,7 @@ function 开始TCP连接世代(remoteConnWrapper) {
821815
return { generation, downlinkDrain };
822816
}
823817

824-
async function 读取XHTTP首包(reader, token) {
818+
async function 读取叉HTTP首包(reader, token) {
825819
const decoder = VLESS文本解码器;
826820

827821
const 尝试解析魏烈思首包 = (data) => {
@@ -2604,6 +2598,87 @@ function 创建Grain收纳器(容量, 复制合包结果 = false) {
26042598
};
26052599
}
26062600

2601+
function 创建上行Grain合包流(目标字节 = 上行合包目标字节) {
2602+
const identity = typeof IdentityTransformStream !== 'undefined'
2603+
? new IdentityTransformStream()
2604+
: new TransformStream();
2605+
const writer = identity.writable.getWriter();
2606+
const 缓冲 = new Uint8Array(目标字节);
2607+
let 缓冲长度 = 0;
2608+
let 定时器 = null;
2609+
let 在途写 = null;
2610+
let 冲刷链 = Promise.resolve();
2611+
2612+
const 清理定时器 = () => {
2613+
if (定时器) {
2614+
clearTimeout(定时器);
2615+
定时器 = null;
2616+
}
2617+
};
2618+
2619+
const 串行写 = async (chunk) => {
2620+
if (在途写) await 在途写;
2621+
在途写 = writer.write(chunk);
2622+
try { await 在途写 } finally { 在途写 = null; }
2623+
};
2624+
2625+
const 冲刷 = async () => {
2626+
if (缓冲长度) {
2627+
const chunk = 缓冲.slice(0, 缓冲长度);
2628+
缓冲长度 = 0;
2629+
await 串行写(chunk);
2630+
}
2631+
};
2632+
2633+
const 排队冲刷 = () => {
2634+
冲刷链 = 冲刷链.then(() => 冲刷()).catch(() => { });
2635+
};
2636+
2637+
const 启动定时器 = () => {
2638+
if (定时器) return;
2639+
定时器 = setTimeout(() => {
2640+
定时器 = null;
2641+
排队冲刷();
2642+
}, 1);
2643+
};
2644+
2645+
return {
2646+
readable: identity.readable,
2647+
写入: async (chunk) => {
2648+
const data = 数据转Uint8Array(chunk);
2649+
if (!data.byteLength) return;
2650+
if (data.byteLength >= 目标字节) {
2651+
清理定时器();
2652+
if (缓冲长度) await 冲刷();
2653+
await 串行写(data);
2654+
return;
2655+
}
2656+
if (缓冲长度 + data.byteLength >= 目标字节) {
2657+
const output = new Uint8Array(缓冲长度 + data.byteLength);
2658+
output.set(缓冲.subarray(0, 缓冲长度), 0);
2659+
output.set(data, 缓冲长度);
2660+
缓冲长度 = 0;
2661+
清理定时器();
2662+
await 串行写(output);
2663+
} else {
2664+
缓冲.set(data, 缓冲长度);
2665+
缓冲长度 += data.byteLength;
2666+
启动定时器();
2667+
}
2668+
},
2669+
结束: async () => {
2670+
清理定时器();
2671+
try {
2672+
await 冲刷链;
2673+
await 冲刷();
2674+
await writer.close();
2675+
} finally {
2676+
try { writer.releaseLock() } catch (e) { }
2677+
}
2678+
}
2679+
};
2680+
}
2681+
26072682
function 创建上行写入队列({ 获取写入器, 获取连接任务 = null, 释放写入器, 重试连接, 关闭连接, 名称 = '上行队列' }) {
26082683
const grain = 创建Grain收纳器(上行合包目标字节);
26092684
let draining = false;
@@ -4707,9 +4782,16 @@ function base64SecretDecode(encoded, secret) {
47074782

47084783
function 获取传输协议配置(配置 = {}) {
47094784
const 是gRPC = 配置.传输协议 === 'grpc';
4710-
const { : 本机Padding头, : 本机Padding键 } = 获取XHTTPPadding标识(配置.UUID);
4785+
const { : 本机Padding头, : 本机Padding键 } = 获取叉HTTPPadding标识(配置.UUID);
4786+
const 叉混淆JSON = {
4787+
"xPaddingObfsMode": true,
4788+
"xPaddingMethod": "tokenish",
4789+
"xPaddingPlacement": "queryInHeader",
4790+
"xPaddingHeader": 本机Padding头,
4791+
"xPaddingKey": 本机Padding键
4792+
};
47114793
return {
4712-
type: 是gRPC ? (配置.gRPC模式 === 'multi' ? 'grpc&mode=multi' : 'grpc&mode=gun') : (配置.传输协议 === 'xhttp' ? `xhttp&mode=stream-one&extra=%7B%22xPaddingObfsMode%22%3Atrue%2C%22xPaddingMethod%22%3A%22tokenish%22%2C%22xPaddingPlacement%22%3A%22queryInHeader%22%2C%22xPaddingHeader%22%3A%22${本机Padding头}%22%2C%22xPaddingKey%22%3A%22${本机Padding键}%22%7D` : 'ws'),
4794+
type: 是gRPC ? (配置.gRPC模式 === 'multi' ? 'grpc&mode=multi' : 'grpc&mode=gun') : (配置.传输协议 === 'xhttp' ? `xhttp&mode=stream-one&extra=${encodeURIComponent(JSON.stringify(叉混淆JSON))}` : 'ws'),
47134795
路径字段名: 是gRPC ? 'serviceName' : 'path',
47144796
域名字段名: 是gRPC ? 'authority' : 'host'
47154797
};

0 commit comments

Comments
 (0)