From 1eb0359b8f6994e93eb868e8f9d68446acbbc4ea Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Wed, 24 Jun 2026 00:19:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20hysteria2=20=E5=B8=A6=20pinSHA256=20?= =?UTF-8?q?=E6=97=B6=E8=B7=B3=E8=BF=87=E8=AF=81=E4=B9=A6=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 带 pinSHA256 的 hysteria2 节点用自签/SNI 伪装证书(如 sni=www.bing.com), 靠指纹固定校验而非标准 CA 链。Surge 的 server-cert-fingerprint-sha256 实测 不可靠(仍按 SNI 主机名校验并拒绝),故改为输出 skip-cert-verify=true, 与 Shadowrocket 等客户端的实际行为一致。 新增 regen-surge-lines.ts 一次性脚本:从 uri 重新生成所有静态节点的 surge_line(保留自定义名字),修正解析器改动前已存的节点。 Co-Authored-By: Claude Opus 4.8 --- server/src/parsers/hysteria2.ts | 8 ++++- server/src/scripts/regen-surge-lines.ts | 43 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 server/src/scripts/regen-surge-lines.ts diff --git a/server/src/parsers/hysteria2.ts b/server/src/parsers/hysteria2.ts index b4ee8ba..fcb4b30 100644 --- a/server/src/parsers/hysteria2.ts +++ b/server/src/parsers/hysteria2.ts @@ -18,11 +18,17 @@ export function parseHysteria2(uri: string): ParsedNode { const sni = params.get('sni') || ''; const obfs = params.get('obfs') || ''; const obfsPassword = params.get('obfs-password') || params.get('obfs_password') || ''; + // pinSHA256 means the node uses a self-signed / SNI-masqueraded cert verified by + // fingerprint rather than the standard CA chain. Surge's server-cert-fingerprint-sha256 + // proved unreliable here (it still validates against the SNI hostname and rejects the + // cert), so in that case we skip standard verification — matching the effective behavior + // of clients like Shadowrocket that connect fine via pinSHA256. + const hasPin = !!(params.get('pinSHA256') || params.get('pinsha256')); let line = `${name} = hysteria2, ${server}, ${port}, password=${auth}`; if (sni) line += `, sni=${sni}`; if (obfs === 'salamander' && obfsPassword) line += `, salamander-password=${obfsPassword}`; - line += `, skip-cert-verify=${readInsecure(params)}`; + line += `, skip-cert-verify=${readInsecure(params) || hasPin}`; return { name, type: 'hysteria2', server, port, surgeLine: line }; } diff --git a/server/src/scripts/regen-surge-lines.ts b/server/src/scripts/regen-surge-lines.ts new file mode 100644 index 0000000..095f158 --- /dev/null +++ b/server/src/scripts/regen-surge-lines.ts @@ -0,0 +1,43 @@ +// One-off migration: regenerate static_nodes.surge_line from their stored uri +// using the current parsers. Static nodes cache surge_line at add-time, so a +// parser fix (e.g. hysteria2 skip-cert-verify) does not reach already-stored +// nodes until they are re-parsed. The custom node name is preserved. +// +// Run inside the container: pnpm exec tsx src/scripts/regen-surge-lines.ts +import db from '../db.js'; +import { parseNodeUri } from '../parsers/index.js'; + +function renameSurgeLine(surgeLine: string, oldName: string, newName: string): string { + if (surgeLine.startsWith(oldName + ' = ')) { + return newName + surgeLine.slice(oldName.length); + } + return surgeLine; +} + +const rows = db.prepare('SELECT id, name, uri, surge_line FROM static_nodes').all() as Array<{ + id: number; + name: string; + uri: string; + surge_line: string; +}>; + +const update = db.prepare('UPDATE static_nodes SET surge_line = ?, type = ? WHERE id = ?'); +let changed = 0; + +for (const row of rows) { + const node = parseNodeUri(row.uri); + if (!node) { + console.warn(`[skip] id=${row.id} "${row.name}" — unparseable uri`); + continue; + } + const newLine = renameSurgeLine(node.surgeLine, node.name, row.name); + if (newLine !== row.surge_line) { + update.run(newLine, node.type, row.id); + changed++; + console.log(`[updated] id=${row.id} "${row.name}"`); + console.log(` old: ${row.surge_line}`); + console.log(` new: ${newLine}`); + } +} + +console.log(`\nDone. ${changed}/${rows.length} static node(s) regenerated.`);