Skip to content

Bug Analysis: Clash Meta ech-opts.config not parsed — complete root cause and fix #1174

Description

@xtgm

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 / 其余链路正常

  1. StandardV2RayBean.java — The echConfig field exists and is properly serialized:

    public Boolean enableECH;
    public String echConfig;  // ← field exists, just never populated from Clash YAML
  2. 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
            }
        }
    }
  3. SingBoxOptions.javaOutboundECHOptions 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.ktwhen 块中添加 "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.echConfigV2RayFmt.kt splits by lines → OutboundECHOptions.config → sing-box core.

这是一行代码的修复。echConfig 值通过现有链路传递:
bean.echConfigV2RayFmt.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="]
    }
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions