Skip to content

Commit 3dd9f14

Browse files
committed
feat: ECH
1 parent 3106f70 commit 3dd9f14

9 files changed

Lines changed: 48 additions & 2 deletions

File tree

REPO_SCHEMA.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ Android 端的代码主要分为两个核心包:
3333
- debug 级别输出脱敏的 `Outbound chain ... appToEgress=` 链拓扑摘要,仅含实体 ID、协议类型与服务器端点,不含 UUID、密码、Reality 公钥或其他认证材料,用于区分链式代理 EOF 发生在哪一跳。
3434
- 支持 Shadowsocks, VMess, Trojan, Hysteria, Juicity, Naive, WireGuard 等协议的配置解析与转换。
3535
- `forTest`(URL 测速)配置不生成 `experimental` 块:官方内核在 `PlatformLogWriter != nil` 时无条件创建 CacheFile(bbolt)与 ClashServer,无显式 `path` 时所有实例共用工作目录(`no_backup`)下的 `cache.db`。主进程批量测速并发创建大量 `TestInstance` 曾共享该文件,bbolt freelist 被写坏后每次 `box.Start` 的清理 batch 在 bbolt 定时器 goroutine 中 `page already freed` panic(异步 goroutine 中无法 recover)→ 主进程 SIGABRT 闪退。最终修复在 Go 侧:测速实例走 [`NewTestSingBoxInstance`](libcore/box.go)(不注册 PlatformLogWriter → 官方 `needCacheFile`/`needClashAPI` 均不触发),测速完全不产生 cache.db;[`SagerNet.onCreate`](app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt)(main/bg 进程)启动时仍清扫存量共享 `cache.db` 与历史残留 `urltest_*.db` 实现老用户自愈。另:测速拨号依赖平台接口监视器,`NativeInterface.startDefaultInterfaceMonitor` 必须同步注册(异步曾致首拨竞态秒报 `no available network interface`),`SagerNet` 两个进程常驻 `DefaultNetworkListener` 预热缓存。**测速配置须与正式连接逐项对齐**(对齐 husi):`ipv6Mode` 与 outbound `domain_strategy` 均沿用用户设置——曾分别强制 `IPv6Mode.ENABLE` 与空串,测速拨号的协议族/解析结果与真实路径不同,造成"测速 err 实际能用、测速成功实际不能用"的双向失真;DNS 侧 forTest 保持 dns-direct 收尾(与正式配置中服务器域名经 dns 规则归 dns-direct 的解析路径一致),无 fakeip/sniff/路由规则/experimental 属测速本就不需要之合理差异。
36-
- `SingBoxOutboundParser.kt`: 将 sing-box 配置中的单个 outbound JSON 还原为原生协议 Bean(支持 shadowsocks/vmess/vless/trojan/hysteria/hysteria2/tuic/socks/http/wireguard/anytls,含 TLS/transport/multiplex 子块解析)。用于订阅返回完整 sing-box 配置(含 `outbounds`)的场景:`RawUpdater.parseJSON``outbounds` 分支对每个 outbound 优先调用 `parseSingBoxOutbound()` 还原原生节点,不支持的类型或解析失败时回退为 `ConfigBean`(自定义 JSON);`dns`/`block`/`direct`/`selector`/`urltest` 类型的 outbound 始终跳过。
36+
- `SingBoxOutboundParser.kt`: 将 sing-box 配置中的单个 outbound JSON 还原为原生协议 Bean(支持 shadowsocks/vmess/vless/trojan/hysteria/hysteria2/tuic/socks/http/wireguard/anytls,含 TLS/transport/multiplex 子块解析,TLS 的 `ech` 子块含 `config``query_server_name`)。用于订阅返回完整 sing-box 配置(含 `outbounds`)的场景:`RawUpdater.parseJSON``outbounds` 分支对每个 outbound 优先调用 `parseSingBoxOutbound()` 还原原生节点,不支持的类型或解析失败时回退为 `ConfigBean`(自定义 JSON);`dns`/`block`/`direct`/`selector`/`urltest` 类型的 outbound 始终跳过。
37+
- ECH 分享链接参数:`parseDuckSoft`(trojan/vless/vmess ducksoft 格式)解析 `ech=` 查询参数——社区格式 `ech=<ECH查询域名>+<DoH地址>`(Xray echConfigList 风格)或 `ech=true/1`,存在且非 `none/0/false` 时置 `enableECH=true``+` 前域名部分写入 `StandardV2RayBean.echQueryServerName`(DoH 部分丢弃:sing-box 不支持为 ECH 查询单独指定 DoH,走自身 DNS 路由);`buildSingBoxOutboundTLS` 将其输出为 sing-box 1.13+ 的 `ech.query_server_name`(为空时内核用 `server_name` 查询 HTTPS 记录);`toUriVMessVLESSTrojan` 导出时 `echQueryServerName` 非空写 `ech=<域名>`、否则写 `ech=true`。Bean 序列化版本 11(v11 起附带 `echQueryServerName`)。
3738
- [`io/nekohasekai/sagernet/ui/`](app/src/main/java/io/nekohasekai/sagernet/ui): 各种 Activity 和 Fragment 界面。
3839
- `MainActivity.kt`: 应用主界面。预览版启动时弹出提示对话框(标题为 `BuildConfig.PRE_VERSION_NAME`),提供「不再显示」按钮(`preview_hint_dont_show_again`):点击后把当前版本号写入 `DataStore.previewHintDismissedVersion``Key.PREVIEW_HINT_DISMISSED_VERSION`),之后仅对已忽略的版本不再弹窗,发布新预览版时会重新提示。
3940
- `SettingsFragment.kt`: 设置界面。「入站设置」:TUN 模式(Android 10+)下只要混合入站存在,`VpnService` 即无条件 `setHttpProxy` 指向本地代理端口(原 nb4a「追加 HTTP 代理至 VPN」开关 `appendHttpProxy` 已移除——其作用与混合入站存在性重叠,回环访问控制改由认证字段表达)。「配置身份验证」项(`mixedAuthConfig`,仅 UI 查找键)点击弹出用户名/密码对话框:两项均留空则混合入站免认证(本机回环免密直连),用户名非空则 `ConfigBuilder` 为 mixed 入站配置该账密(`DataStore.mixedInboundNeedsAuth`,仅 TUN 模式);原 nb4a 时代自动生成随机 `mixedSecret` 的隐藏鉴权机制已移除,app 内部组件(订阅更新/资产下载/版本检查经 `DataStore.mixedInboundUser/Pass`)跟随运行时 `mixedInboundAuthed` 自动带票。「禁用混合入站」开关(`disableMixedInbound`,见 `SettingsPreferenceFragment.kt`):仅在 TUN 模式下真正生效(`DataStore.mixedInboundDisabled`),开启后 `ConfigBuilder` 不再生成 mixed 入站及其专属的 `inbound = [mixed-in]` 路由规则、不再监听本地代理端口,`VpnService` 同时跳过 `setHttpProxy`;此时「代理端口」「配置身份验证」「HTTP 代理绕过列表」设置项一并变灰,端口摘要显示「已禁用」。在系统代理模式下开启该开关会 Toast 提示"禁用只在TUN模式有效,当前监听端口:xxx"且不影响混合入站。内部 HTTP(订阅/资产/版本检查)统一 [`tryProxyOutbound()`](app/src/main/java/io/nekohasekai/sagernet/ktx/Nets.kt)→ libcore `HTTPClient.TryBoxOutbound` 经 main box 默认 outbound 拨号,不再 `trySocks5(mixedPort)`(纯 TUN 下 mixed 不存在会 fail connect socks5)。

app/src/main/java/io/nekohasekai/sagernet/fmt/SingBoxOutboundParser.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ private class SingBoxTLSFields {
213213
var realityShortId: String = ""
214214
var enableECH: Boolean = false
215215
var echConfig: String = ""
216+
var echQueryServerName: String = ""
216217
var disableSNI: Boolean = false
217218
}
218219

@@ -247,6 +248,7 @@ private fun JSONObject.parseSingBoxTLS(): SingBoxTLSFields? {
247248
is JSONArray -> config.toStringList().joinToString("\n")
248249
else -> ""
249250
}
251+
echQueryServerName = it.getStr("query_server_name") ?: ""
250252
}
251253
}
252254
disableSNI = tls.optBoolean("disable_sni", false)
@@ -265,6 +267,7 @@ private fun StandardV2RayBean.applySingBoxTLS(tls: SingBoxTLSFields?) {
265267
realityShortId = tls.realityShortId
266268
enableECH = tls.enableECH
267269
echConfig = tls.echConfig
270+
echQueryServerName = tls.echQueryServerName
268271
}
269272

270273
// ---------------------------------------------------------------------------

app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ public abstract class StandardV2RayBean extends AbstractBean {
6868

6969
public String echConfig;
7070

71+
// sing-box ech.query_server_name:ECH HTTPS 记录查询域名(链接 ech=<domain>+<doh> 的域名部分)
72+
public String echQueryServerName;
73+
7174
// --------------------------------------- Mux
7275

7376
public Boolean enableMux;
@@ -126,6 +129,7 @@ public void initializeDefaultValues() {
126129

127130
if (enableECH == null) enableECH = false;
128131
if (JavaUtil.isNullOrBlank(echConfig)) echConfig = "";
132+
if (JavaUtil.isNullOrBlank(echQueryServerName)) echQueryServerName = "";
129133

130134
if (enableMux == null) enableMux = false;
131135
if (muxPadding == null) muxPadding = false;
@@ -148,7 +152,7 @@ public void initializeDefaultValues() {
148152

149153
@Override
150154
public void serialize(ByteBufferOutput output) {
151-
output.writeInt(10);
155+
output.writeInt(11);
152156
super.serialize(output);
153157
output.writeString(uuid);
154158
output.writeString(encryption);
@@ -214,6 +218,8 @@ public void serialize(ByteBufferOutput output) {
214218

215219
output.writeBoolean(enableECH);
216220
output.writeString(echConfig);
221+
// v11
222+
output.writeString(echQueryServerName);
217223

218224
output.writeInt(packetEncoding);
219225

@@ -345,6 +351,11 @@ public void deserialize(ByteBufferInput input) {
345351
} // 否则后一位就是 packetEncoding
346352
}
347353

354+
// v11
355+
if (version >= 11) {
356+
echQueryServerName = input.readString();
357+
}
358+
348359
packetEncoding = input.readInt();
349360

350361
if (version >= 2) {

app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) {
203203
url.queryParameter("sid")?.let {
204204
realityShortId = it
205205
}
206+
// 社区格式: ech=<ECH查询域名>+<DoH地址>(Xray echConfigList 风格),或 ech=true/1
207+
url.queryParameter("ech")?.let {
208+
if (it.isNotBlank() && !it.equals("none", true) && it != "0" && !it.equals("false", true)) {
209+
enableECH = true
210+
// sing-box 不支持为 ECH 查询单独指定 DoH(走自身 DNS 路由),仅保留域名部分
211+
val queryDomain = it.substringBefore('+')
212+
if (queryDomain.isNotBlank() && !queryDomain.equals("true", true) && queryDomain != "1") {
213+
echQueryServerName = queryDomain
214+
}
215+
}
216+
}
206217
}
207218
}
208219

@@ -600,6 +611,12 @@ fun StandardV2RayBean.toUriVMessVLESSTrojan(isTrojan: Boolean): String {
600611
builder.addQueryParameter("pbk", realityPubKey)
601612
builder.addQueryParameter("sid", realityShortId)
602613
}
614+
if (enableECH) {
615+
builder.addQueryParameter(
616+
"ech",
617+
echQueryServerName.ifBlank { "true" }
618+
)
619+
}
603620
}
604621
}
605622
}
@@ -800,6 +817,9 @@ fun buildSingBoxOutboundTLS(bean: StandardV2RayBean): OutboundTLSOptions? {
800817
if (bean.enableECH) {
801818
ech = OutboundECHOptions().apply {
802819
enabled = true
820+
if (bean.echQueryServerName.isNotBlank()) {
821+
query_server_name = bean.echQueryServerName
822+
}
803823
if (bean.echConfig.isNotBlank()) {
804824
config = if (bean.echConfig.contains("BEGIN ECH CONFIGS")) {
805825
bean.echConfig.lines()

app/src/main/java/io/nekohasekai/sagernet/ui/profile/StandardV2RaySettingsActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ abstract class StandardV2RaySettingsActivity : ProfileSettingsActivity<StandardV
4848

4949
private val enableECH = pbm.add(PreferenceBinding(Type.Bool, "enableECH"))
5050
private val echConfig = pbm.add(PreferenceBinding(Type.Text, "echConfig"))
51+
private val echQueryServerName = pbm.add(PreferenceBinding(Type.Text, "echQueryServerName"))
5152

5253
private val enableMux = pbm.add(PreferenceBinding(Type.Bool, "enableMux"))
5354
private val muxPadding = pbm.add(PreferenceBinding(Type.Bool, "muxPadding"))

app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,9 @@ public static class OutboundECHOptions extends SingBoxOption {
19971997

19981998
public String config_path;
19991999

2000+
// Since sing-box 1.13.0
2001+
public String query_server_name;
2002+
20002003
}
20012004

20022005
public static class OutboundUTLSOptions extends SingBoxOption {

app/src/main/res/values-zh-rCN/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,7 @@
519519
<string name="enable_ech_sum">启用 ECH</string>
520520
<string name="ech_settings">ECH 设置</string>
521521
<string name="ech_config">ECH 配置</string>
522+
<string name="ech_query_server_name">ECH 查询域名</string>
522523
<string name="http_upgrade_host">HTTPUpgrade 主机</string>
523524
<string name="http_upgrade_path">HTTPUpgrade 路径</string>
524525
<string name="update_current_subscription">更新当前组订阅</string>

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@
607607
<string name="enable_ech_sum">Enable Encrypted Client Hello</string>
608608
<string name="ech_settings">ECH Settings</string>
609609
<string name="ech_config">ECH Config</string>
610+
<string name="ech_query_server_name">ECH Query Server Name</string>
610611
<string name="http_upgrade_host">HTTPUpgrade Host</string>
611612
<string name="http_upgrade_path">HTTPUpgrade Path</string>
612613
<string name="update_current_subscription">Update current Group\'s subscription</string>

app/src/main/res/xml/standard_v2ray_preferences.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,11 @@
274274
app:key="echConfig"
275275
app:title="@string/ech_config"
276276
app:useSimpleSummaryProvider="true" />
277+
<EditTextPreference
278+
app:icon="@drawable/ic_baseline_domain_24"
279+
app:key="echQueryServerName"
280+
app:title="@string/ech_query_server_name"
281+
app:useSimpleSummaryProvider="true" />
277282
</PreferenceCategory>
278283

279284
</PreferenceScreen>

0 commit comments

Comments
 (0)