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) {