Skip to content

Fix imported VLESS Reality nodes with null short-id#30

Open
hmtdpetn wants to merge 1 commit into
black-ant:masterfrom
hmtdpetn:fix/clash-null-map-string
Open

Fix imported VLESS Reality nodes with null short-id#30
hmtdpetn wants to merge 1 commit into
black-ant:masterfrom
hmtdpetn:fix/clash-null-map-string

Conversation

@hmtdpetn

@hmtdpetn hmtdpetn commented Jun 10, 2026

Copy link
Copy Markdown

中文说明

问题说明

这个 PR 修复了一类会导致导入后的 VLESS Reality 节点完全无法使用的问题。

部分 Clash 订阅中的 VLESS Reality 节点可能包含:

reality-opts:
  public-key: example-public-key
  short-id: null

Clash YAML 中的 null 被解析为 Go 的 nil 后,原来的 getMapString() 会继续通过 fmt.Sprint() 将该值转换为字符串。

对于 Go nil,转换结果并不是空字符串,而是:

<nil>

因此,原来的代码可能生成以下无效 Xray 配置:

"realitySettings": {
  "publicKey": "example-public-key",
  "shortId": "<nil>"
}

"<nil>" 不是有效的 Reality shortId,Xray 会拒绝该配置。

在实际使用中,受影响的节点会出现:

  • VLESS Reality 节点无法正常连接;
  • 将节点分配给指纹浏览器后,浏览器没有互联网连接;
  • 软件内置节点测速失败或超时;
  • IP 健康检测失败或超时;
  • Xray 代理桥接无法提供可用的本地端口。

在本次实际测试使用的真实订阅中,所有受该字段影响的 VLESS Reality 节点都会出现上述问题,因此这不是单纯的配置显示错误,而是会直接导致节点不可用。

原因定位

问题不在 VLESS、Reality 或 Xray 本身,而是在 Clash 配置值转换过程中。

原来的 getMapString() 只检查键是否存在:

v, ok := m[key]
if !ok {
    return ""
}

当键存在但值为真正的 Go nil 时,代码会继续执行兜底字符串转换,最终错误得到 "<nil>"

修复方式

本次修改让 getMapString() 同时处理不存在的键和真正的 Go nil

v, ok := m[key]
if !ok || v == nil {
    return ""
}

修复后,当 Clash Reality 的 short-idnull 时:

  • getMapString() 返回空字符串;
  • VLESS Reality 构造逻辑不会写入 shortId
  • 最终 Xray 配置中不会再出现无效的 "shortId": "<nil>"

本次修改只处理真正的 Go nil,不会改变用户明确填写的普通字符串:

"null"
"nil"
"<nil>"

原有字符串、整数、浮点数和布尔值的转换行为也保持不变。

自动测试

新增两组回归测试。

第一组验证通用配置值转换:

  • 真正的 Go nil 返回空字符串;
  • 普通字符串 "null""nil""<nil>" 保持原值;
  • 正常字符串、数字和布尔值仍按原有逻辑转换。

第二组模拟带有空 Reality short-id 的 VLESS 节点,并验证:

  • outbound 可以正常生成;
  • streamSettings.securityreality
  • realitySettings 正常存在;
  • publicKey 等有效字段得到保留;
  • shortId 键被完全省略;
  • 序列化后的 outbound 中不包含 "<nil>"

同一组回归测试在官方原始 master 上会失败:

getMapString("nil") = "<nil>", want ""
shortId should be omitted:
map[... shortId:<nil> ...]

应用本次修复后,同一组测试通过。

测试结果:

PASS: focused regression tests
PASS: go test -count=1 ./backend/internal/proxy
PASS: go test -count=1 ./backend/...

go test -count=1 ./... 在根包处失败,原因是当前源码目录没有生成 frontend/dist

main.go:23:12: pattern all:frontend/dist: no matching files found

这是项目已有的前端嵌入构建前置条件,与本次后端修改无关。

真实订阅人工验证

我已经使用当前修复分支构建出的 Windows EXE,对一份此前存在问题的真实 Clash 订阅进行了端到端测试。

验证结果:

  • 真实 Clash 订阅可以成功导入;
  • 受影响的 VLESS Reality 节点可以正常完成测速;
  • 受影响的 VLESS Reality 节点可以通过 IP 健康检测;
  • 将节点分配给指纹浏览器实例后,可以正常访问互联网;
  • 浏览器中检测到的出口 IP 与所选择的代理节点一致;
  • 在本次测试范围内未发现其他代理类型的功能回归。

在本次实际测试使用的订阅中,所有受该问题影响的 VLESS Reality 节点均已恢复正常。


English Description

Problem

This PR fixes an issue that can make imported VLESS Reality nodes completely unusable.

Some Clash subscriptions contain VLESS Reality nodes with configurations such as:

reality-opts:
  public-key: example-public-key
  short-id: null

After the YAML null value is decoded into a Go nil, the previous implementation of getMapString() continued to convert it through fmt.Sprint().

For a Go nil, the resulting value is not an empty string, but the literal string:

<nil>

As a result, the generated Xray configuration could contain:

"realitySettings": {
  "publicKey": "example-public-key",
  "shortId": "<nil>"
}

"<nil>" is not a valid Reality shortId, so Xray rejects the generated configuration.

In practice, affected nodes can exhibit the following behavior:

  • imported VLESS Reality nodes cannot establish a connection;
  • browser profiles assigned to affected nodes have no Internet access;
  • built-in proxy speed tests fail or time out;
  • IP health checks fail or time out;
  • the Xray bridge cannot provide a usable local proxy port.

In the real subscription used for this verification, every VLESS Reality node affected by this field showed the problem. This is therefore not merely a configuration-display issue; it directly prevents the affected nodes from working.

Root cause

The issue is not caused by VLESS, Reality, or Xray itself. It occurs while converting values from the parsed Clash configuration.

The previous getMapString() implementation only checked whether the key existed:

v, ok := m[key]
if !ok {
    return ""
}

When the key existed but its value was a real Go nil, the function continued to its fallback string conversion and incorrectly returned "<nil>".

Fix

The updated implementation treats both a missing key and a real Go nil as an empty value:

v, ok := m[key]
if !ok || v == nil {
    return ""
}

With this change, when a Clash Reality short-id is null:

  • getMapString() returns an empty string;
  • the VLESS Reality builder does not emit a shortId;
  • the generated Xray configuration no longer contains the invalid value "shortId": "<nil>".

The change only handles real Go nil values. It intentionally preserves explicitly supplied strings such as:

"null"
"nil"
"<nil>"

Existing conversion behavior for strings, integers, floating-point values, and booleans is also preserved.

Automated tests

Two groups of regression tests were added.

The first group verifies generic configuration-value conversion:

  • a real Go nil returns an empty string;
  • the normal strings "null", "nil", and "<nil>" are preserved;
  • valid strings, numbers, and booleans retain their existing conversion behavior.

The second group simulates a VLESS Reality node with a null short-id and verifies that:

  • the outbound is generated successfully;
  • streamSettings.security is reality;
  • realitySettings is present;
  • valid fields such as publicKey are preserved;
  • the shortId key is completely omitted;
  • the serialized outbound does not contain "<nil>".

The same regression tests fail against the original upstream master:

getMapString("nil") = "<nil>", want ""
shortId should be omitted:
map[... shortId:<nil> ...]

The tests pass after applying this fix.

Test results:

PASS: focused regression tests
PASS: go test -count=1 ./backend/internal/proxy
PASS: go test -count=1 ./backend/...

go test -count=1 ./... fails in the root package because frontend/dist has not been generated:

main.go:23:12: pattern all:frontend/dist: no matching files found

This is an existing frontend embed build requirement and is unrelated to this backend change.

Manual verification with a real subscription

I built a Windows EXE from the current fix branch and completed end-to-end verification using a real Clash subscription that previously exhibited the issue.

Verification results:

  • The real Clash subscription was imported successfully.
  • Affected VLESS Reality nodes completed the built-in speed tests successfully.
  • Affected VLESS Reality nodes passed the IP health checks.
  • Browser profiles using the affected nodes could access the Internet normally.
  • The browser-reported exit IP matched the selected proxy node.
  • No regression was observed for other proxy types within the scope of this test.

In the real subscription used for this test, all VLESS Reality nodes affected by this issue were restored to normal operation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant