From e1e6c5ddb0b568e16587566c3e50126e5c150231 Mon Sep 17 00:00:00 2001 From: Tylerrrkd Date: Wed, 5 Aug 2026 10:35:37 +0800 Subject: [PATCH] Fix infinite TUN routing loop on traffic to the TUN's own addresses (#9897) * Restore the silently dropped embedded TUN routing rules tun_singbox_rules declares "network": "udp" as a bare string, but Rule4Sbox.network is a List. System.Text.Json therefore throws on the very first rule, and JsonUtils.Deserialize swallows it and returns default. GenRouting only checks for null before AddRange, so the whole embedded rule set is dropped without a trace. Both rules it carries have thus never reached a generated config: the NetBIOS/mDNS reject and the multicast reject. The mismatch predates the sing-box 1.12 migration - the template has used the string form since the rules file was introduced. - declare network as an array so the template matches Rule4Sbox * Drop traffic addressed to the TUN's own addresses With auto_route the TUN steals the default route, so a packet whose destination is the TUN interface's own address is handed to sing-box instead of being looped back by the kernel. Routing then matches ip_is_private and sends it to `direct`, whose interface is auto-detected as the TUN again, so the packet is written straight back into the TUN and re-enters routing. The loop never terminates and pins a CPU core. Seen in the wild on macOS: a WebRTC client offered the TUN's own fc00::172:18:0:1 ULA as an ICE candidate and the resulting STUN connectivity checks sustained ~8k packets/s out of the interface at 800% CPU, 21 GB written to the TUN over five hours - against 3 GB read, the asymmetry that gives the loop away. Nothing legitimate is addressed to those addresses, so reject them before any outbound rule can match. Use method "drop" rather than the default ICMP unreachable, whose destination would be the looping address itself. - reject the TUN inbound's own addresses, taken from the generated inbound so the two cannot drift apart --- .../Singbox/CoreConfigSingboxServiceTests.cs | 67 +++++++++++++++++++ v2rayN/ServiceLib/Sample/tun_singbox_rules | 4 +- .../Singbox/SingboxRoutingService.cs | 16 +++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs index 96b4c600..b07bef28 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs @@ -626,4 +626,71 @@ public class CoreConfigSingboxServiceTests } } + [Fact] + public void GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules() + { + // The embedded tun rules reject local-network noise (NetBIOS/mDNS, multicast). + // They are deserialized into List, so a schema mismatch in the + // embedded template makes JsonUtils.Deserialize return null and silently + // drops every one of them. + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + config.TunModeItem.EnableTun = true; + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + IsTunEnabled = true, + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.route.rules.Should().Contain( + r => r.action == "reject" + && r.network != null && r.network.Contains("udp") + && r.port != null && r.port.Contains(5353), + "the embedded tun rules must reject mDNS/NetBIOS noise"); + cfg.route.rules.Should().Contain( + r => r.action == "reject" + && r.ip_cidr != null && r.ip_cidr.Contains("224.0.0.0/3"), + "the embedded tun rules must reject multicast traffic"); + } + + [Fact] + public void GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses() + { + // Regression test: traffic addressed to the TUN interface's own addresses must + // never reach an outbound. auto_route hijacks the default route, so `direct` + // writes such a packet straight back into the TUN, which routes it to the + // outbound again - an infinite loop that pins a CPU core. Observed in the wild + // with WebRTC ICE connectivity checks against the TUN's own fc00::/7 ULA + // address, sustaining ~8k packets/s out of the TUN interface. + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + config.TunModeItem.EnableTun = true; + config.TunModeItem.EnableIPv6Address = true; + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + IsTunEnabled = true, + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var tun = cfg.inbounds.First(i => i.type == "tun"); + tun.address.Should().NotBeNullOrEmpty(); + + foreach (var address in tun.address!) + { + cfg.route.rules.Should().Contain( + r => r.action == "reject" && r.ip_cidr != null && r.ip_cidr.Contains(address), + $"traffic to the TUN's own address '{address}' must be rejected, not routed"); + } + } } diff --git a/v2rayN/ServiceLib/Sample/tun_singbox_rules b/v2rayN/ServiceLib/Sample/tun_singbox_rules index a4276134..a4c8b177 100644 --- a/v2rayN/ServiceLib/Sample/tun_singbox_rules +++ b/v2rayN/ServiceLib/Sample/tun_singbox_rules @@ -1,6 +1,8 @@ [ { - "network": "udp", + "network": [ + "udp" + ], "port": [ 135, 137, diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 0ba2d08f..9cc4a169 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -43,6 +43,22 @@ public partial class CoreConfigSingboxService _coreConfig.route.rules.AddRange(tunRules); } + // Traffic addressed to the TUN interface's own addresses must never reach an + // outbound. auto_route hijacks the default route, so `direct` writes such a + // packet straight back into the TUN, which hands it to the outbound again - + // an infinite loop that pins a CPU core. Drop instead of rejecting so no + // ICMP unreachable is generated back towards the same addresses. + var tunAddresses = _coreConfig.inbounds.FirstOrDefault(i => i.type == "tun")?.address; + if (tunAddresses?.Count > 0) + { + _coreConfig.route.rules.Add(new() + { + ip_cidr = [.. tunAddresses], + action = "reject", + method = "drop", + }); + } + var lstDirectExe = BuildRoutingDirectExe(); if (lstDirectExe.Count > 0) {