Skip to content

Commit 29cbc5c

Browse files
authored
Merge pull request #1442 from cmliu/beta2.1
Beta2.1
2 parents 2c8dd8f + 0e97120 commit 29cbc5c

2 files changed

Lines changed: 104 additions & 3 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.20260810030901] - 2026-08-10 03:09:01
2+
3+
### ADD
4+
5+
- 新增 **XHTTP obfs padding** 接收端支持:识别 `xPaddingObfsMode`(tokenish / queryInHeader)混淆头。
6+
17
## [2.1.20260809231057] - 2026-08-09 23:10:57
28

39
### Debug

_worker.js

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const Version = '2026-08-09 23:10:57';
1+
const Version = '2026-08-10 03:09:01';
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';
@@ -71,7 +71,10 @@ export default {
7171
} else if (管理员密码 && !访问路径.startsWith('admin/') && 访问路径 !== 'login' && request.method === 'POST') {// gRPC/XHTTP代理
7272
const 反代上下文 = await 反代参数获取(url, userID, 默认反代IP, 默认反代兜底);
7373
const referer = request.headers.get('Referer') || '';
74-
const 命中XHTTP特征 = referer.includes('x_padding', 14) || referer.includes('x_padding=');
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键);
7578
if (!命中XHTTP特征 && contentType.startsWith('application/grpc')) {
7679
log(`[gRPC] 命中请求: ${url.pathname}${url.search}`);
7780
return await 处理gRPC请求(request, userID, 反代上下文);
@@ -528,8 +531,92 @@ export default {
528531
}
529532
};
530533
///////////////////////////////////////////////////////////////////////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,
541+
const HPACKHuffman码长 = [
542+
13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28,
543+
28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28,
544+
6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6,
545+
5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10,
546+
13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
547+
7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6,
548+
15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5,
549+
6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28,
550+
20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23,
551+
24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24,
552+
22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23,
553+
21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23,
554+
26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25,
555+
19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27,
556+
20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23,
557+
26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26,
558+
30
559+
];
560+
561+
// 由 UUID 推导本机 XHTTP padding 头名/键名(与链接生成处 extra 的 xPaddingHeader / xPaddingKey 完全一致)
562+
function 获取XHTTPPadding标识(yourUUID) {
563+
return { : yourUUID.slice(1, 7), : '_' + yourUUID.slice(1, 7) };
564+
}
565+
566+
// 计算字符串经 HPACK Huffman 编码后的字节长度(向上取整;与官方 hpack.HuffmanEncodeLength 等价,按 UTF-8 字节遍历)
567+
function 计算HPACKHuffman字节长度(字符串) {
568+
const 字节 = new TextEncoder().encode(字符串);
569+
let 总位数 = 0;
570+
for (let i = 0; i < 字节.length; i++) {
571+
总位数 += HPACKHuffman码长[字节[i]];
572+
}
573+
return Math.ceil(总位数 / 8);
574+
}
575+
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键) {
580+
const 头值 = request.headers.get(本机Padding头);
581+
if (头值) {
582+
try {
583+
const 解析URL = new URL(头值, 'https://x.invalid');
584+
const 查询值 = 解析URL.searchParams.get(本机Padding键);
585+
if (查询值) return 查询值;
586+
} catch (e) { }
587+
return 头值;
588+
}
589+
const 请求URL = new URL(request.url);
590+
return 请求URL.searchParams.get(本机Padding键) || '';
591+
}
592+
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 请求,直接放行
598+
const huffman长度 = 计算HPACKHuffman字节长度(padding值);
599+
// tokenish:huffman 编码字节长度须在 [100-2, 1000+2] 容差范围内
600+
return huffman长度 >= 98 && huffman长度 <= 1002;
601+
}
602+
603+
// 生成随机 base62 padding 串(用于响应端 padding,官方 GeneratePadding tokenish 的简化版;客户端不校验响应 padding)
604+
const XHTTPBase62字符集 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
605+
function 生成XHTTPPadding串(长度) {
606+
const 字符集长度 = XHTTPBase62字符集.length;
607+
let 结果 = '';
608+
for (let i = 0; i < 长度; i++) {
609+
结果 += XHTTPBase62字符集[Math.floor(Math.random() * 字符集长度)];
610+
}
611+
return 结果;
612+
}
613+
531614
async function 处理XHTTP请求(request, yourUUID, 反代上下文 = {}) {
532615
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 });
533620
const reader = request.body.getReader();
534621
const 首包 = await 读取XHTTP首包(reader, yourUUID);
535622
if (!首包) {
@@ -557,6 +644,13 @@ async function 处理XHTTP请求(request, yourUUID, 反代上下文 = {}) {
557644
'X-Accel-Buffering': 'no',
558645
'Cache-Control': 'no-store'
559646
});
647+
// 响应端 padding(官方 ApplyXPaddingToResponse 对应,obfs queryInHeader:头名=本机Padding头,值为含 query 的 URL 形式)
648+
// 客户端不校验响应 padding,仅作响应特征混淆;随机长度 100~1000
649+
try {
650+
const 响应URL = new URL('https://x.invalid/');
651+
响应URL.searchParams.set(本机Padding键, 生成XHTTPPadding串(100 + Math.floor(Math.random() * 901)));
652+
responseHeaders.set(本机Padding头, 响应URL.toString());
653+
} catch (e) { }
560654

561655
// UDP 分支:拆到独立函数(保留原逻辑)
562656
if (首包.isUDP) return 处理XHTTPUDP请求(首包, reader, request, 反代上下文, responseHeaders);
@@ -4613,8 +4707,9 @@ function base64SecretDecode(encoded, secret) {
46134707

46144708
function 获取传输协议配置(配置 = {}) {
46154709
const 是gRPC = 配置.传输协议 === 'grpc';
4710+
const { : 本机Padding头, : 本机Padding键 } = 获取XHTTPPadding标识(配置.UUID);
46164711
return {
4617-
type: 是gRPC ? (配置.gRPC模式 === 'multi' ? 'grpc&mode=multi' : 'grpc&mode=gun') : (配置.传输协议 === 'xhttp' ? 'xhttp&mode=stream-one' : 'ws'),
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'),
46184713
路径字段名: 是gRPC ? 'serviceName' : 'path',
46194714
域名字段名: 是gRPC ? 'authority' : 'host'
46204715
};

0 commit comments

Comments
 (0)