SagerNet / SagerNet/sing-box

selector 的 interrupt_exist_connections 对经路由的连接不生效(切换所选成员后存量连接不被中断)

Open
#4,281 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
38.1k
Forks
4.6k
Avg merge
19d 15h
Merged PRs (30d)
1

Description

操作系统

macOS(注:本问题与平台无关,纯 selector 逻辑,任意平台可复现)

系统版本

macOS 26.5.2 (arm64)

安装类型

sing-box 原始命令行程序

版本
sing-box version 1.14.0-alpha.41

(同样在 1.14.0-alpha.40 复现;两版行为一致)
描述

selector 开启 interrupt_exist_connections: true 后,通过 SelectOutbound(clash API PUT /proxies/{selector} 或 gRPC)切换所选成员时,经旧成员建立的存量连接不会被中断,仍持续在旧成员上传输。

  • 预期:切换所选成员后,此前经该 selector 拨出到旧成员的存量连接应被中断(RST),以便在新成员上重建。
  • 实际:存量连接全部存活,GET /connections 显示它们仍挂在旧成员(chains 未变),now 已切到新成员,但对存量连接无效。

疑似根因(源码分析,供参考):selector 的 interrupt_exist_connections 只中断注册进该 selector 的 interrupt group 的连接,而注册仅发生在 Selector.DialContext → interruptGroup.NewConn(legacy dialer 路径)。但经路由承接的连接走的是 ConnectionHandler 路径(Selector.NewConnection → 取所选成员 outbound 直接 NewConnection),绕过了上述注册。于是 SelectOutbound 触发的 interruptGroup.Interrupt() 遍历的是空集,对这些连接无效。故凡 selector 作为「路由目标 / final」承接入站流量(而非作为其它组件的 dialer/detour)时,该选项对用户流量近乎 no-op。

重现方式

纯本地、命令行、direct 出站,不依赖任何远程服务器 / TUN / 图形界面:本地起一个只 accept 不回应的 TCP 汇(127.0.0.1:9999)用来把连接「挂住」,经 mixed 入站的 HTTP 代理连到它,再切换 selector 观察存量连接。

最简配置 config.json

{
  "log": { "level": "info", "timestamp": true },
  "inbounds": [
    { "type": "mixed", "listen": "127.0.0.1", "listen_port": 18091 }
  ],
  "outbounds": [
    { "type": "selector", "tag": "sel", "outbounds": ["memA", "memB"], "default": "memA", "interrupt_exist_connections": true },
    { "type": "direct", "tag": "memA" },
    { "type": "direct", "tag": "memB" }
  ],
  "route": { "final": "sel" },
  "experimental": { "clash_api": { "external_controller": "127.0.0.1:19091" } }
}

步骤:

  1. 起一个只 accept、不回应的本地 TCP 汇(把连接挂住):
    python3 -c "import socket; s=socket.socket(); s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1); s.bind(('127.0.0.1',9999)); s.listen(50); h=[]
    while True: c,_=s.accept(); h.append(c)"
    
  2. sing-box run -c config.json
  3. 经 mixed 入站建立 3 条持久连接(连本地 9999,被挂住):
    for i in 1 2 3; do curl -s -x http://127.0.0.1:18091 -o /dev/null --max-time 40 http://127.0.0.1:9999/ & done
    
  4. 确认连接经 memAcurl -s http://127.0.0.1:19091/connections —— 3 条,chains 均为 ["memA","sel"]now = memA
  5. 切换 selector 到 memBcurl -s -X PUT http://127.0.0.1:19091/proxies/sel -d '{"name":"memB"}'
  6. 再查:curl -s http://127.0.0.1:19091/connections —— 3 条连接仍在 memA(chains 未变),未被中断,而 now 已是 memB(bug)。
日志

连接建立日志(3 条经 mixed 入站,127.0.0.1 → 127.0.0.1:9999,全本地):

2026-07-10 09:39:04 INFO [3308771764 0ms] inbound/mixed[0]: inbound connection from 127.0.0.1:63071
2026-07-10 09:39:04 INFO [1881857641 0ms] inbound/mixed[0]: inbound connection from 127.0.0.1:63072
2026-07-10 09:39:04 INFO [586386173 0ms] inbound/mixed[0]: inbound connection from 127.0.0.1:63073
2026-07-10 09:39:04 INFO [586386173 1ms] inbound/mixed[0]: inbound connection to 127.0.0.1:9999
2026-07-10 09:39:04 INFO [3308771764 2ms] inbound/mixed[0]: inbound connection to 127.0.0.1:9999
2026-07-10 09:39:04 INFO [1881857641 1ms] inbound/mixed[0]: inbound connection to 127.0.0.1:9999

/connections 切换前后对比(now 已切到 memB,但 3 条存量连接 chains 仍全为 ["memA","sel"],未被中断):

### BEFORE  (now = memA) ###
活 3 条,chains 全为 ["memA","sel"]

### PUT /proxies/sel  {"name":"memB"}   → now = memB ###

### AFTER  (now = memB) ###
活 3 条,chains: [["memA","sel"], ["memA","sel"], ["memA","sel"]]   ← 仍全在 memA,未被中断
完整性要求
  • 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
  • 我保证提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
  • 我保证提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器、TUN、图形界面客户端或者其他闭源软件。
  • 我保证提供了完整的配置文件与日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the issue with the provided config and trace selector paths from SelectOutbound through Selector.NewConnection and Selector.DialContext, including interruptGroup.NewConn. Compare how routed connections are registered with the interrupt group, then verify that switching from memA to memB interrupts the existing connections instead of leaving them in /connections.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.