SagerNet / SagerNet/sing-box

auto_redirect 接收端口被访问时,route(direct) 触发无限自拨号回环。

Open
#4,339 2 comments 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

操作系统

Linux

系统版本

Ubuntu 26.04 LTS(Resolute Raccoon)

安装类型

sing-box 原始命令行程序

如果您使用图形客户端程序,请提供该程序版本。

No response

版本
sing-box version 1.14.0-beta.1

Environment: go1.24.7 linux/amd64
Revision: 8bc6787c7ff785e5f6343241affdadd5ca239bd7
CGO: disabled
该二进制由 2026-07-23 同步后的 `SagerNet/sing-box` `origin/testing` 源码自行编译,HEAD 为 `v1.14.0-beta.1` / `8bc6787c7ff785e5f6343241affdadd5ca239bd7`。
描述

当 TCP 客户端访问 sing-box 自动创建的 auto_redirect TCP 接收端口,且恢复出的原始目标为“本机或本地路由地址 + 当前 auto_redirect 接收端口”时,可以稳定触发无限自拨号回环。

本次本地隔离复现中:

  1. auto_redirect 随机监听端口为 3723,监听在 0.0.0.0:3723
  2. network namespace 客户端只发起一次 10.255.255.2:51810 -> 10.255.254.1:3723 连接。
  3. 第一轮进入 inbound/tun[tun-in],原始目标仍为 10.255.254.1:3723
  4. 连接命中 ip_is_private=true => route(direct)
  5. direct 出站再次连接 10.255.254.1:3723
  6. 从第二轮开始,来源变为本机 10.255.254.1:<随机端口>,目标始终为 10.255.254.1:3723
  7. 每一轮都会再次进入同一个 auto_redirect listener、命中同一条路由规则并再次 direct 拨号。
  8. 只发起一次客户端连接,日志中却出现了 39 次相同目标入站和 39 次相同目标 direct 出站,其中 38 次入站来源为本机。
  9. 本次测试设置了 ulimit -n 256,循环最终在约 1 ms 内触发 socket: too many open files。如果没有文件描述符和日志限制,可能持续消耗文件描述符、CPU、conntrack 和磁盘空间。

auto_detect_interface: true 没有阻止该场景。

与 #4086 的关系

该问题与 #4086 的“TUN + direct 出站导致回环/高 CPU”症状相似,但本 Issue 的触发条件更加明确且可以在本地确定性复现:

  • 不依赖远程节点、DNS、规则集、测速或长时间负载;
  • 只需一次 TCP 连接;
  • 目标端口明确等于当前 auto_redirect TCP 接收端口;
  • 可以直接观察到第一轮来自客户端,后续轮次全部来自 sing-box 自身 direct 拨号。
源码侧观察

当前依赖的 sing-tun 版本为:

github.com/sagernet/sing-tun v0.8.12-0.20260722035624-8cededca4cc1

相关代码路径:

  • sing-tun/redirect_linux.go:未指定自定义端口时,在 IPv4 unspecified address(0.0.0.0)和随机端口上启动 redirect server;
  • sing-tun/redirect_server.goAcceptTCP() 后调用 control.GetOriginalDestination(conn),随后无递归检查地调用 handler.NewConnectionEx(...)
  • sing-tun/redirect_nftables.goinet4_local_address_set 只从接口地址生成,本地路由但未配置到接口的地址不会进入集合;
  • sing-box/common/dialer/default.go:direct 的默认 dialer 会附加 auto_redirect output mark。

需要特别说明:实测的后续 direct 套接字会被设置 0x2024 output mark,通常会在 nftables output 链提前 return,因此后续连接不一定需要再次执行 redirect。但其目标本来就是 0.0.0.0:<auto_redirect 端口> 所监听的地址和端口,所以即使绕过 nftables redirect,连接仍会直接进入同一个 redirect listener。listener 恢复出的目标仍是自身地址和端口,随后又被路由到 direct,形成递归。

换言之,output mark 能避免“普通 direct 流量再次被重定向”,但不能避免“direct 直接拨打 auto_redirect listener 自身”。

期望行为

当恢复出的原始目标同时满足以下条件时:

  1. 目标地址属于本机或本地路由地址;
  2. 目标端口等于当前 auto_redirect TCP 接收端口;

sing-box 不应继续通过 direct 出站拨打自己的 auto_redirect 接收端口。程序应 bypass、reject,或通过其他方式阻止递归。

建议在 redirect listener 或路由前增加自身监听端点检查,或者增加递归深度/相同目标熔断,避免单个外部 SYN 导致无限资源消耗。

重现方式

以下命令均以 root 执行。复现前确认没有其他 sing-box 实例管理 inet sing-box 表。

本问题的对象就是 TUN auto_redirect,因此最简配置不可移除 sing-box 自身的 TUN inbound;除此之外不依赖远程服务器、代理节点、DNS、规则集、图形客户端或闭源组件。

1. 内核模块
modprobe tun
modprobe nfnetlink_queue
modprobe nft_queue

lsmod | grep -E 'nfnetlink_queue|nft_queue|tun'

本机实际输出:

nft_queue              12288  0
nfnetlink_queue        32768  0
nf_tables             409600  1 nft_queue
nfnetlink              20480  4 nfnetlink_queue,nf_tables

TUN 在本机内核中为 built-in,因此没有单独出现在 lsmod 中。测试日志中没有 nfqueue not availablepre-match disabled,并且 nftables 中存在 queue flags bypass to 100,说明本次 NFQUEUE 可用。

2. 创建隔离客户端网络
ip netns add sb-client

ip link add sb-host type veth peer name sb-client
ip addr add 10.255.255.1/30 dev sb-host
ip link set sb-host up

ip link set sb-client netns sb-client
ip netns exec sb-client ip link set lo up
ip netns exec sb-client ip addr add 10.255.255.2/30 dev sb-client
ip netns exec sb-client ip link set sb-client up

ip netns exec sb-client \
  ip route add 10.255.254.1/32 via 10.255.255.1
3. 创建未配置到接口、但由本机处理的目标地址

auto_detect_interface: true 在本机把 direct socket 绑定到默认出接口 ens3。若将测试 local route 指向 lo,direct 拨号会在 5 秒后超时,只产生一轮,不会复现。因此本机成功复现使用与自动检测结果一致的 ens3

ip route add table local local 10.255.254.1/32 dev ens3

ip address | grep 10.255.254.1
ip route show table local | grep 10.255.254.1
ip route get 10.255.254.1

实际结果:

# ip address | grep 10.255.254.1
# 无输出

# ip route show table local | grep 10.255.254.1
local 10.255.254.1 dev ens3 scope host

# ip route get 10.255.254.1
local 10.255.254.1 dev lo src 10.255.254.1 uid 0
    cache <local>

其他机器应将 ens3 替换为 auto_detect_interface 实际选择的默认出接口。

4. 最简 sing-box 配置

保存为 /tmp/sing-box-auto-redirect-loop.json

{
  "log": {
    "level": "debug",
    "timestamp": true
  },
  "inbounds": [
    {
      "type": "tun",
      "tag": "tun-in",
      "address": [
        "172.19.0.1/30"
      ],
      "auto_route": true,
      "strict_route": true,
      "auto_redirect": true,
      "dns_mode": "disabled"
    }
  ],
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct"
    }
  ],
  "route": {
    "rules": [
      {
        "ip_is_private": true,
        "outbound": "direct"
      }
    ],
    "final": "direct",
    "auto_detect_interface": true
  }
}

配置检查:

sing-box check -c /tmp/sing-box-auto-redirect-loop.json

当前版本检查成功,不需要修改任何字段。

5. 受限启动
bash -c '
  ulimit -n 256
  exec sing-box run \
    -c /tmp/sing-box-auto-redirect-loop.json
' >/tmp/sing-box-auto-redirect-loop.log 2>&1 &

SB_PID=$!

for i in $(seq 1 50); do
  nft list table inet sing-box >/dev/null 2>&1 && break
  sleep 0.1
done

PORT="$(
  nft list table inet sing-box |
  sed -n 's/.*redirect to :\([0-9][0-9]*\).*/\1/p' |
  head -n 1
)"

echo "auto_redirect port: $PORT"
ps -fp "$SB_PID"
ss -lntp | grep ":${PORT}"
nft -a list table inet sing-box \
  >/tmp/sing-box-auto-redirect-nft.txt
nft list set inet sing-box inet4_local_address_set

本次实际端口及监听:

auto_redirect port: 3723
LISTEN 0      16384        0.0.0.0:3723       0.0.0.0:*    users:(("sing-box",pid=266617,fd=10))

本机地址集合:

table inet sing-box {
    set inet4_local_address_set {
        type ipv4_addr
        flags interval
        elements = { 10.255.255.0/30, 127.0.0.0/8,
                     172.19.0.0/30, 185.184.223.0/24 }
    }
}

集合包含真实接口网段 10.255.255.0/30、loopback、TUN 网段及默认出接口网段,但不包含只有 local route、未配置到接口的 10.255.254.1

nftables 关键规则:

chain prerouting_prematch {
    type filter hook prerouting priority dstnat - 1; policy accept;
    meta mark 0x00002024 ct mark set meta mark counter packets 0 bytes 0 return
    ct mark 0x00002024 return
    ct mark 0x00002023 return
    tcp flags & (syn | ack) == syn counter packets 698 bytes 35860 queue flags bypass to 100
}

chain output_prematch {
    type filter hook output priority mangle - 1; policy accept;
    meta mark 0x00002024 ct mark set meta mark counter packets 483 bytes 670390 return
    ct mark 0x00002024 return
    ct mark 0x00002023 return
    tcp flags & (syn | ack) == syn counter packets 63 bytes 4336 queue flags bypass to 100
}

chain output {
    type nat hook output priority mangle + 1; policy accept;
    meta mark 0x00002024 counter packets 35 bytes 2325 return
    ct mark 0x00002024 counter packets 0 bytes 0 return
    ip daddr @inet4_local_address_set counter packets 23 bytes 1421 return
    meta nfproto ipv4 meta l4proto tcp counter packets 10 bytes 520 redirect to :3723 return
}

chain prerouting {
    type nat hook prerouting priority dstnat + 1; policy accept;
    ct mark 0x00002024 counter packets 0 bytes 0 return
    iifname "tun0" counter packets 0 bytes 0 return
    ip daddr @inet4_local_address_set counter packets 692 bytes 36163 return
    meta nfproto ipv4 meta l4proto tcp counter packets 0 bytes 0 redirect to :3723 return
}
6. 只发起一次客户端连接并保持 3 秒
ip netns exec sb-client \
  python3 - "$PORT" <<'PY'
import socket
import sys
import time

port = int(sys.argv[1])

sock = socket.create_connection(
    ("10.255.254.1", port),
    timeout=3,
)

time.sleep(3)
sock.close()
PY

命令完成后立即停止:

kill "$SB_PID" 2>/dev/null || true
sleep 0.2
kill -9 "$SB_PID" 2>/dev/null || true
wait "$SB_PID" 2>/dev/null || true
7. 统计结果
grep -c \
  "inbound connection to 10.255.254.1:${PORT}" \
  /tmp/sing-box-auto-redirect-loop.log

grep -c \
  "outbound connection to 10.255.254.1:${PORT}" \
  /tmp/sing-box-auto-redirect-loop.log

grep -c \
  "inbound redirect connection from 10.255.255.2:" \
  /tmp/sing-box-auto-redirect-loop.log

grep -c \
  "inbound redirect connection from 10.255.254.1:" \
  /tmp/sing-box-auto-redirect-loop.log

实际结果:

相同目标入站:39
相同目标 direct 出站:39
客户端来源:1
本机自连接来源:38
8. bypass 对照

仅将规则改为:

{
  "ip_is_private": true,
  "action": "bypass",
  "outbound": "direct"
}

其余配置、local route、文件描述符限制和单次 3 秒客户端连接均不变。对照实例的随机 auto_redirect 端口为 7763

实际结果与原先“bypass 应切断循环”的预期不符:

相同目标入站:42
相同目标 direct 出站:42
客户端来源:1
本机自连接来源:41
too many open files 日志:5
ss -ant 行数:18 -> 67
nf_conntrack_count:31 -> 81

日志明确显示 bypass(direct) 后仍由 direct 拨打相同监听端口:

+0800 2026-07-24 00:11:52 INFO [2692202996 0ms] inbound/tun[tun-in]: inbound redirect connection from 10.255.255.2:59178
+0800 2026-07-24 00:11:52 INFO [2692202996 0ms] inbound/tun[tun-in]: inbound connection to 10.255.254.1:7763
+0800 2026-07-24 00:11:52 DEBUG [2692202996 0ms] router: match[0] ip_is_private=true => bypass(direct)
+0800 2026-07-24 00:11:52 INFO [2692202996 0ms] outbound/direct[direct]: outbound connection to 10.255.254.1:7763
+0800 2026-07-24 00:11:52 INFO [84296351 0ms] inbound/tun[tun-in]: inbound redirect connection from 10.255.254.1:6136
+0800 2026-07-24 00:11:52 INFO [84296351 0ms] inbound/tun[tun-in]: inbound connection to 10.255.254.1:7763
+0800 2026-07-24 00:11:52 DEBUG [84296351 0ms] router: match[0] ip_is_private=true => bypass(direct)
+0800 2026-07-24 00:11:52 INFO [84296351 0ms] outbound/direct[direct]: outbound connection to 10.255.254.1:7763

这说明在“目标就是 listener 自身端口”的场景,NFQUEUE bypass/output mark 只能绕过重定向,不能阻止 direct 直接连接 wildcard listener。本问题不能仅依赖用户添加 bypass 规则规避。

9. 清理
ip route del table local local 10.255.254.1/32 dev ens3 \
  2>/dev/null || true
ip netns del sb-client 2>/dev/null || true
ip link del sb-host 2>/dev/null || true
nft delete table inet sing-box 2>/dev/null || true
日志
触发前不存在其他 sing-box 进程。本次仅执行一次 namespace 客户端连接。

第一轮客户端连接及随后两轮本机自连接:

+0800 2026-07-23 23:55:29 INFO [2563400103 0ms] inbound/tun[tun-in]: inbound redirect connection from 10.255.255.2:51810
+0800 2026-07-23 23:55:29 INFO [2563400103 0ms] inbound/tun[tun-in]: inbound connection to 10.255.254.1:3723
+0800 2026-07-23 23:55:29 DEBUG [2563400103 0ms] router: match[0] ip_is_private=true => route(direct)
+0800 2026-07-23 23:55:29 INFO [2563400103 0ms] outbound/direct[direct]: outbound connection to 10.255.254.1:3723
+0800 2026-07-23 23:55:29 INFO [840279609 0ms] inbound/tun[tun-in]: inbound redirect connection from 10.255.254.1:49330
+0800 2026-07-23 23:55:29 INFO [840279609 0ms] inbound/tun[tun-in]: inbound connection to 10.255.254.1:3723
+0800 2026-07-23 23:55:29 DEBUG [840279609 0ms] router: match[0] ip_is_private=true => route(direct)
+0800 2026-07-23 23:55:29 INFO [840279609 0ms] outbound/direct[direct]: outbound connection to 10.255.254.1:3723
+0800 2026-07-23 23:55:29 INFO [2596850063 0ms] inbound/tun[tun-in]: inbound redirect connection from 10.255.254.1:49340
+0800 2026-07-23 23:55:29 INFO [2596850063 0ms] inbound/tun[tun-in]: inbound connection to 10.255.254.1:3723
+0800 2026-07-23 23:55:29 DEBUG [2596850063 0ms] router: match[0] ip_is_private=true => route(direct)
+0800 2026-07-23 23:55:29 INFO [2596850063 0ms] outbound/direct[direct]: outbound connection to 10.255.254.1:3723

中间继续以新的本机源端口重复完全相同的四行模式。最后一轮:

+0800 2026-07-23 23:55:29 INFO [551465335 0ms] inbound/tun[tun-in]: inbound redirect connection from 10.255.254.1:49754
+0800 2026-07-23 23:55:29 INFO [551465335 0ms] inbound/tun[tun-in]: inbound connection to 10.255.254.1:3723
+0800 2026-07-23 23:55:29 DEBUG [551465335 0ms] router: match[0] ip_is_private=true => route(direct)
+0800 2026-07-23 23:55:29 INFO [551465335 0ms] outbound/direct[direct]: outbound connection to 10.255.254.1:3723
+0800 2026-07-23 23:55:29 ERROR [551465335 1ms] connection: open connection to 10.255.254.1:3723 using outbound/direct[direct]: dial tcp 10.255.254.1:3723: socket: too many open files
支持我们
完整性要求
  • 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
  • 我保证提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
  • 我保证提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器、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

First reproduce the loop with the supplied TUN auto_redirect configuration and inspect the generated nftables rules. Read sing-tun/redirect_linux.go, sing-tun/redirect_server.go, sing-tun/redirect_nftables.go, and sing-box/common/dialer/default.go to trace listener acceptance, original-destination recovery, and direct dialing. Done means a connection targeting the auto_redirect listener cannot recursively create direct connections or exhaust resources, with the reproduction no longer showing repeated self-connections.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.