SagerNet / SagerNet/sing-box

OpenWrt 上 IPv6 UDP 经 direct 出站重新进入 TUN,单个数据包被放大为持续路由回环。

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

Nobody has claimed this yet.

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

Description

操作系统

Linux

系统版本

ImmortalWrt 24.10.6 r33869-cf234f8de6d5 Linux 6.6.133 架构:aarch64 目标平台:rockchip/armv8

安装类型

sing-box 原始命令行程序

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

No response

版本
sing-box version 1.14.0-alpha.46
Environment: go1.25.12 linux/arm64
Tags:
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_usbip,badlinkname,tfogo_checklinkname0,with_musl
Revision: 8cbc2e4e1379d260764ad9884330da2433b1d4d3
CGO: enabled
描述

在 OpenWrt/ImmortalWrt 上使用 TUN inbound,并同时开启 auto_route、auto_redirect、strict_route 和 route.auto_detect_interface 后,
发往公网 IPv6 地址的 UDP 流量经 direct outbound 出站时会重新进入 sing-box 自己的 TUN。

一个局域网客户端只需偶尔发送一个 1~16 字节的 UDP 数据包,进入 sing-box 后就会被放大成约 200 个新 UDP packet connection/秒,并持续循环。

实际表现:

  1. 在 br-lan/eth1 上,原始客户端数秒甚至十几秒才发送一个 UDP 包。
  2. 在 tun0 上,同一目的地址和相同 UDP payload 会在约 0.47 秒内出现 100 次。
  3. 每次循环都会生成新的随机 UDP 源端口。
  4. sing-box 因此将每次循环识别为新的 packet connection。
  5. 每轮产生多条 inbound、sniff、route 和 outbound 日志。
  6. 日志曾在约 17 分钟内增长超过 400 MiB,轮转时达到约 541 MiB、410 万行。
  7. 将日志级别改为 warn 只能隐藏现象,不能终止数据包循环。

原始客户端并没有以该速率发送流量,放大发生在 sing-box TUN/direct 路径内部。

这个问题与以下 issue 很相似:

https://github.com/SagerNet/sing-box/issues/4086

但本问题能够通过 IPv6 UDP 小包稳定触发,并且在 1.14.0-alpha.46(revision 8cbc2e4e)上仍然存在。

初步定位到的疑似源码原因如下。

direct outbound 使用 common/dialer 创建默认 Dialer:

protocol/direct/outbound.go:51-74

当 route.auto_detect_interface=true 时,common/dialer/default.go 原本会安装 AutoDetectInterfaceFunc:

common/dialer/default.go:93-135

但是,只要 auto_redirect 注册了 autoRedirectOutputMark,route/network.go 中的 AutoDetectInterfaceFunc 就会直接返回 nil,不再执行
BindToInterface:

return func(network, address string, conn syscall.RawConn) error {
if r.autoRedirectOutputMark != 0 {
return nil
}
return bindFunc(network, address, conn)
}

位置:

route/network.go:345-375

随后只对 socket 设置 auto-redirect output mark:

return control.RoutingMark(r.autoRedirectOutputMark)(network, address, conn)

位置:

route/network.go:394-412

在本机 OpenWrt IPv6 环境中,WAN 默认路由是按公网源前缀匹配的 source-specific route。

当 UDP direct socket 没有绑定 pppoe-wan,也没有预先选择公网 IPv6 源地址时,仅依靠 fwmark 无法保证选中 PPPoE 的 source-specific
IPv6 默认路由。该 socket 最终选择了 TUN 地址 fdfe:dcba:9876::1,并再次进入 table 2022。

由于 direct outbound 的 loopback 棹查只判断“目的地址是否属于本机 TUN 网段”,而本问题的目的地址是公网 IPv6,因此无法阻止这种“公网目的地址被内核重新路由回 TUN”的循环。

相关代码:

protocol/direct/outbound.go:115-145

预期行为:

route.auto_detect_interface=true 应保证 direct outbound 不会重新进入 sing-box 自己的 TUN。

即使启用了 auto_redirect,也应该满足以下至少一种行为:

  1. direct outbound 仍绑定到检测出的默认物理接口;
  2. mark 路由能够确保从物理 WAN 表出站,包括 OpenWrt 的 IPv6 source-specific route;
  3. 在 direct socket 实际解析回 sing-box 自己的 TUN 时检测并拒绝该连接。

目前已知的临时规避方法是给公网 direct outbound 显式添加:

"bind_interface": "pppoe-wan"

已有 #4086 报告该方法能够阻止回环。

但是,同一个 direct outbound 往往还承担 ip_is_private/LAN 直连。直接绑定 pppoe-wan 可能破坏局域网访问,因此需要额外拆分 LAN direct 和 WAN direct。

重现方式

使用下面的完整最简配置。该配置不依赖代理服务器,也不需要目的 UDP 服务端返回任何数据;只需要系统具有可用的公网 IPv6 路由。

config.json:

{
  "log": {
    "level": "debug",
    "timestamp": true
  },
  "inbounds": [
    {
      "type": "tun",
      "tag": "tun-in",
      "interface_name": "tun-test",
      "address": [
        "172.19.0.1/30",
        "fdfe:dcba:9876::1/126"
      ],
      "auto_route": true,
      "auto_redirect": true,
      "strict_route": true
    }
  ],
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct"
    }
  ],
  "route": {
    "auto_detect_interface": true,
    "final": "direct"
  }
}

启动:

sing-box check -c config.json
sing-box run -c config.json

从局域网客户端向任意可路由的公网 IPv6 地址发送一个很小的 UDP 包。

本次实际触发使用的目标为:

[240e:930:1000:500:800::3e]:20001
[240e:f7:8013:409:800::e]:20002

例如:

printf x | nc -u -w 1 240e:930:1000:500:800::3e 20001

同时抓取 LAN 和 TUN:

tcpdump -ni br-lan -e -vv \
  'ip6 and udp and dst host 240e:930:1000:500:800::3e and dst port 20001'
tcpdump -ni tun-test -vv \
  'ip6 and udp and dst host 240e:930:1000:500:800::3e and dst port 20001'

实际结果:

  • br-lan 上只出现客户端原始 UDP 包。
  • TUN 中相同 payload 持续重复出现。
  • 每轮使用不同的随机源端口。
  • sing-box 日志持续生成新的 inbound packet connection。
  • 即使客户端不以高频率继续发送,TUN 内仍会在一段时间内保持高频循环。

本机实际抓包中,100 个 TUN 数据包约 0.47 秒完成,约为 200 包/秒。

路由对照:

ip -6 route get 240e:930:1000:500:800::3e

结果:

240e:930:1000:500:800::3e from :: via fdfe:dcba:9876::2 dev tun0 table 2022 src fdfe:dcba:9876::1

指定 PPPoE 公网源地址:

ip -6 route get 240e:930:1000:500:800::3e from <WAN_PUBLIC_IPV6>

结果:

240e:930:1000:500:800::3e from <WAN_PUBLIC_IPV6> via <WAN_GATEWAY> dev pppoe-wan

指定 LAN 获得的公网 IPv6:

ip -6 route get 240e:930:1000:500:800::3e from <LAN_PUBLIC_IPV6>

结果同样通过 pppoe-wan。

指定 TUN 源地址:

ip -6 route get 240e:930:1000:500:800::3e from fdfe:dcba:9876::1

结果:

240e:930:1000:500:800::3e from fdfe:dcba:9876::1 via fdfe:dcba:9876::2 dev tun0 table 2022

建议的对照实验:

将 direct outbound 临时改为:

{
  "type": "direct",
  "tag": "direct",
  "bind_interface": "pppoe-wan"
}

重新启动后执行相同 UDP 测试。

预期对照结果是数据包只从 pppoe-wan 发出,不再重新进入 TUN。

日志
以下为循环期间的连续日志片段。连接 ID 和源端口每次变化,但目标地址、目标端口和 payload 保持一致:

  +0800 2026-07-17 22:18:17 INFO [3875553281 0ms] inbound/tun[0]: inbound packet connection from [fdfe:dcba:9876::1]:35994
  +0800 2026-07-17 22:18:17 INFO [3875553281 0ms] inbound/tun[0]: inbound packet connection to [240e:930:1000:500:800::3e]:20001
  +0800 2026-07-17 22:18:17 DEBUG [3875553281 0ms] router: match[3] => sniff
  +0800 2026-07-17 22:18:17 DEBUG [3875553281 0ms] router: match[15] rule_set=[geoip-cn ...] => route(直连)
  +0800 2026-07-17 22:18:17 INFO [3875553281 0ms] outbound/direct[直连]: outbound packet connection

  随后立即出现新的随机源端口:

  +0800 2026-07-17 22:18:17 INFO [NEW_CONNECTION_ID 0ms] inbound/tun[0]: inbound packet connection from
  [fdfe:dcba:9876::1]:NEW_RANDOM_PORT
  +0800 2026-07-17 22:18:17 INFO [NEW_CONNECTION_ID 0ms] inbound/tun[0]: inbound packet connection to
  [240e:930:1000:500:800::3e]:20001
  +0800 2026-07-17 22:18:17 DEBUG [NEW_CONNECTION_ID 0ms] router: match[3] => sniff
  +0800 2026-07-17 22:18:17 DEBUG [NEW_CONNECTION_ID 0ms] router: match[15] rule_set=[geoip-cn ...] => route(直连)
  +0800 2026-07-17 22:18:17 INFO [NEW_CONNECTION_ID 0ms] outbound/direct[直连]: outbound packet connection

实际 LAN 抓包:

5c:ea:1d:xx:xx:xx > ROUTER_MAC, ethertype IPv6:
LAN_PUBLIC_IPV6.13728 > 240e:930:1000:500:800::3e.20001:
UDP, length 1

约 12 秒内只捕获到一个对应 LAN 包。

实际 TUN 抓包:

fdfe:dcba:9876::1.48663 > 240e:930:1000:500:800::3e.20001: UDP, length 1
fdfe:dcba:9876::1.41459 > 240e:930:1000:500:800::3e.20001: UDP, length 1
fdfe:dcba:9876::1.38228 > 240e:930:1000:500:800::3e.20001: UDP, length 1
fdfe:dcba:9876::1.39461 > 240e:930:1000:500:800::3e.20001: UDP, length 1
fdfe:dcba:9876::1.48841 > 240e:930:1000:500:800::3e.20001: UDP, length 1

100 packets captured
214 packets received by filter
0 packets dropped by kernel

100 个包约在 0.47 秒内完成。

防火墙计数器同时显示大量流量来自 tun0:

iifname "tun0" counter packets 2032033 bytes 380267339 accept

轮转前日志统计:

567697722 bytes
4102830 lines

该日志从 22:00 左右开始迅速增长,到 22:17 轮转时达到约 541.4 MiB。

支持我们
完整性要求
  • 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
  • 我保证提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
  • 我保证提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器、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 minimal IPv6 UDP loop with the provided config, using ip -6 route get and tcpdump on br-lan and tun-test. Read protocol/direct/outbound.go, common/dialer/default.go, and route/network.go at the cited ranges, then trace auto_detect_interface and auto_redirect together. Done means direct traffic no longer re-enters the TUN while LAN direct routing remains usable, with a regression test or documented validation for the reproduced case.

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
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.