Bug Description / 问题描述
When importing proxies from Clash Meta YAML subscriptions, the ech-opts.config field is silently dropped. Only ech-opts.enable is parsed. This causes ECH to be "enabled" but without the actual ECH configuration data, making ECH completely non-functional.
通过 Clash Meta YAML 订阅导入节点时,ech-opts.config 字段被静默丢弃。仅解析了 ech-opts.enable。这导致 ECH 虽然显示"已启用",但缺少实际的 ECH 配置数据,ECH 功能完全无法工作。
Related issues / 相关 issue: #1115, #1131
Root Cause Analysis / 根因分析
The Data Flow / 数据流
Clash YAML RawUpdater.kt StandardV2RayBean V2RayFmt.kt sing-box JSON
────────── ───────────── ───────────────── ─────────── ─────────────
ech-opts:
enable: true → bean.enableECH=true enableECH=true → ech.enabled=true ✅ OK
config: "AEX.." (NOT READ) echConfig="" → ech.config=(empty) ❌ LOST
The Bug Location / Bug 位置
File: app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt
Lines: 481-488
// Current code (buggy)
"ech-opts" -> (opt.value as? Map<String, Any?>)?.also {
for (echOpt in it) {
when (echOpt.key) {
"enable" -> bean.enableECH =
echOpt.value.toString() == "true"
// ← "config" case is MISSING
}
}
}
The when block only handles "enable". The "config" key is never read, so bean.echConfig remains empty ("").
when 块只处理了 "enable","config" 键从未被读取,因此 bean.echConfig 始终为空字符串。
The Rest of the Pipeline Works / 其余链路正常
-
StandardV2RayBean.java — The echConfig field exists and is properly serialized:
public Boolean enableECH;
public String echConfig; // ← field exists, just never populated from Clash YAML
-
V2RayFmt.kt (buildSingBoxOutboundTLS) — Correctly uses echConfig when present:
if (bean.enableECH) {
ech = OutboundECHOptions().apply {
enabled = true
if (bean.echConfig.isNotBlank()) {
config = bean.echConfig.lines() // ← works if echConfig has data
}
}
}
-
SingBoxOptions.java — OutboundECHOptions has the config field:
public static class OutboundECHOptions extends SingBoxOption {
public Boolean enabled;
public List<String> config;
public String config_path;
}
The entire pipeline from Bean → sing-box JSON is already implemented and working. The only missing piece is the parsing step in RawUpdater.kt.
从 Bean 到 sing-box JSON 的整条链路已经实现且正常工作。唯一缺失的就是 RawUpdater.kt 中的解析步骤。
Verification / 验证
Test 1: sing-box format subscription (works)
When NekoBox imports a sing-box JSON subscription (with "outbounds" array), the ECH config is stored as-is in a ConfigBean and passed directly to sing-box core. ECH works correctly.
当 NekoBox 导入 sing-box JSON 格式订阅时,ECH 配置被原样存储并直接传递给 sing-box 内核,ECH 正常工作。
Test 2: Clash YAML subscription (broken)
When NekoBox imports a Clash Meta YAML subscription with ech-opts.config, the config value is lost during parsing. sing-box receives ech.enabled=true but no config data, so ECH fails.
当 NekoBox 导入包含 ech-opts.config 的 Clash Meta YAML 订阅时,config 值在解析过程中丢失。sing-box 收到 ech.enabled=true 但没有 config 数据,ECH 失败。
Test 3: husi fork (works)
The husi fork (a NekoBox derivative based on the same sing-box core) correctly handles ECH config in PEM format, confirming that the sing-box core fully supports ECH — the issue is purely in NekoBox's Clash YAML parser.
husi 分支(基于相同 sing-box 内核的 NekoBox 衍生版)正确处理了 PEM 格式的 ECH 配置,证实 sing-box 内核完全支持 ECH——问题纯粹在 NekoBox 的 Clash YAML 解析器中。
Fix / 修复方案
Add a "config" branch to the when block in RawUpdater.kt:
在 RawUpdater.kt 的 when 块中添加 "config" 分支:
"ech-opts" -> (opt.value as? Map<String, Any?>)?.also {
for (echOpt in it) {
when (echOpt.key) {
"enable" -> bean.enableECH =
echOpt.value.toString() == "true"
"config" -> bean.echConfig =
echOpt.value?.toString() ?: ""
}
}
}
This is a one-line fix. The echConfig value flows through the existing pipeline:
bean.echConfig → V2RayFmt.kt splits by lines → OutboundECHOptions.config → sing-box core.
这是一行代码的修复。echConfig 值通过现有链路传递:
bean.echConfig → V2RayFmt.kt 按行分割 → OutboundECHOptions.config → sing-box 内核。
PR: #1173
Clash Meta YAML ECH Example / Clash Meta YAML ECH 示例
For reference, this is what a Clash Meta proxy with ECH looks like:
proxies:
- name: "example-node"
type: vless
server: example.com
port: 443
uuid: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
tls: true
client-fingerprint: chrome
network: ws
ws-opts:
path: /path
headers:
Host: example.com
ech-opts:
enable: true
config: "AEX+DQBBNAAgACD4KRZ3rChRTE9KO064u+QgcTb1TU54hj6SNXTOPWk+VgAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA="
After the fix, config will be correctly parsed and passed to sing-box as:
{
"tls": {
"ech": {
"enabled": true,
"config": ["AEX+DQBBNAAgACD4KRZ3rChRTE9KO064u+QgcTb1TU54hj6SNXTOPWk+VgAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA="]
}
}
}
Bug Description / 问题描述
When importing proxies from Clash Meta YAML subscriptions, the
ech-opts.configfield is silently dropped. Onlyech-opts.enableis parsed. This causes ECH to be "enabled" but without the actual ECH configuration data, making ECH completely non-functional.通过 Clash Meta YAML 订阅导入节点时,
ech-opts.config字段被静默丢弃。仅解析了ech-opts.enable。这导致 ECH 虽然显示"已启用",但缺少实际的 ECH 配置数据,ECH 功能完全无法工作。Related issues / 相关 issue: #1115, #1131
Root Cause Analysis / 根因分析
The Data Flow / 数据流
The Bug Location / Bug 位置
File:
app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.ktLines: 481-488
The
whenblock only handles"enable". The"config"key is never read, sobean.echConfigremains empty ("").when块只处理了"enable","config"键从未被读取,因此bean.echConfig始终为空字符串。The Rest of the Pipeline Works / 其余链路正常
StandardV2RayBean.java— TheechConfigfield exists and is properly serialized:V2RayFmt.kt(buildSingBoxOutboundTLS) — Correctly usesechConfigwhen present:SingBoxOptions.java—OutboundECHOptionshas theconfigfield:The entire pipeline from Bean → sing-box JSON is already implemented and working. The only missing piece is the parsing step in
RawUpdater.kt.从 Bean 到 sing-box JSON 的整条链路已经实现且正常工作。唯一缺失的就是
RawUpdater.kt中的解析步骤。Verification / 验证
Test 1: sing-box format subscription (works)
When NekoBox imports a sing-box JSON subscription (with
"outbounds"array), the ECH config is stored as-is in aConfigBeanand passed directly to sing-box core. ECH works correctly.当 NekoBox 导入 sing-box JSON 格式订阅时,ECH 配置被原样存储并直接传递给 sing-box 内核,ECH 正常工作。
Test 2: Clash YAML subscription (broken)
When NekoBox imports a Clash Meta YAML subscription with
ech-opts.config, the config value is lost during parsing. sing-box receivesech.enabled=truebut no config data, so ECH fails.当 NekoBox 导入包含
ech-opts.config的 Clash Meta YAML 订阅时,config 值在解析过程中丢失。sing-box 收到ech.enabled=true但没有 config 数据,ECH 失败。Test 3: husi fork (works)
The husi fork (a NekoBox derivative based on the same sing-box core) correctly handles ECH config in PEM format, confirming that the sing-box core fully supports ECH — the issue is purely in NekoBox's Clash YAML parser.
husi 分支(基于相同 sing-box 内核的 NekoBox 衍生版)正确处理了 PEM 格式的 ECH 配置,证实 sing-box 内核完全支持 ECH——问题纯粹在 NekoBox 的 Clash YAML 解析器中。
Fix / 修复方案
Add a
"config"branch to thewhenblock inRawUpdater.kt:在
RawUpdater.kt的when块中添加"config"分支:This is a one-line fix. The
echConfigvalue flows through the existing pipeline:bean.echConfig→V2RayFmt.ktsplits by lines →OutboundECHOptions.config→ sing-box core.这是一行代码的修复。
echConfig值通过现有链路传递:bean.echConfig→V2RayFmt.kt按行分割 →OutboundECHOptions.config→ sing-box 内核。PR: #1173
Clash Meta YAML ECH Example / Clash Meta YAML ECH 示例
For reference, this is what a Clash Meta proxy with ECH looks like:
After the fix,
configwill be correctly parsed and passed to sing-box as:{ "tls": { "ech": { "enabled": true, "config": ["AEX+DQBBNAAgACD4KRZ3rChRTE9KO064u+QgcTb1TU54hj6SNXTOPWk+VgAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA="] } } }