From e101b1d7b0eb364efd4a1b623082a857a868583d Mon Sep 17 00:00:00 2001 From: liuclare Date: Sun, 9 Aug 2026 20:15:18 +0800 Subject: [PATCH] Always route IPv6 into Xray TUN regardless of EnableIPv6Address (#9930) * Always route IPv6 into Xray TUN regardless of EnableIPv6Address EnableIPv6Address controls whether the TUN interface is assigned an IPv6 address, but it also gated whether ::/0 was added to autoSystemRoutingTable. With the default (false), IPv6 had no route pointing at the TUN device and followed the system default route instead, leaving the tunnel unproxied and exposing the host's real IPv6 address. The embedded template SampleTunInbound already declares both families; the generated config discarded it. #9843 restored ::/0 only inside the EnableIPv6Address == true branch, so the false branch still leaks. Route both families unconditionally and let the option control only the interface address. The same conditional existed a second time in the RouteExcludeAddress branch and is fixed as well. Fixes #9929 Co-Authored-By: Claude Opus 5 (1M context) * Add regression tests for IPv6 routing in the Xray TUN inbound Both assertions fail on 31044f44 and pass with the fix: Tun_ShouldRouteIPv6IntoTunnel(enableIPv6Address: False) Expected collection {"0.0.0.0/0"} to contain "::/0". TunRouteExcludeAddress_ShouldIncludeIPv6Ranges Expected collection {...44 IPv4 ranges...} to have an item matching x.Contains(:). The theory also covers enableIPv6Address: true, which passes on both revisions, so the tests only fail while the defect is present. The gateway count assertion pins the intended split of responsibilities: EnableIPv6Address governs the interface address, never the routing table. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: liuclare <177657698+liuclare@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) --- .../CoreConfig/CoreConfigTestFactory.cs | 8 ++++ .../V2ray/CoreConfigV2rayServiceTests.cs | 45 +++++++++++++++++++ .../CoreConfig/V2ray/V2rayInboundService.cs | 17 +++---- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs index 4109ea6e..cb901252 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs @@ -251,4 +251,12 @@ internal static class CoreConfigTestFactory config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"]; return config; } + + public static Config CreateConfigWithTun(ECoreType coreType, bool enableIPv6Address) + { + var config = CreateConfig(coreType); + config.TunModeItem.EnableTun = true; + config.TunModeItem.EnableIPv6Address = enableIPv6Address; + return config; + } } diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs index 73f3218e..bad78c06 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs @@ -570,6 +570,51 @@ public class CoreConfigV2rayServiceTests directOutbound!.streamSettings.sockopt!.domainStrategy.Should().Be("UseIPv4"); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GenerateClientConfigContent_Tun_ShouldRouteIPv6IntoTunnel(bool enableIPv6Address) + { + var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray); + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun"); + + tunInbound.Should().NotBeNull(); + tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0"); + tunInbound.settings.autoSystemRoutingTable.Should().Contain("::/0"); + + // EnableIPv6Address governs the interface address only, never the routing table. + tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1); + } + + [Fact] + public void GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges() + { + var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray); + config.TunModeItem.EnableIPv6Address = false; + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray); + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun"); + + tunInbound.Should().NotBeNull(); + tunInbound!.settings.autoSystemRoutingTable.Should().Contain(x => x.Contains(':')); + } + [Fact] public void GenerateClientConfigContent_TunRouteExcludeAddress() { diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs index 22c8826a..515ee917 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs @@ -67,12 +67,14 @@ public partial class CoreConfigV2rayService var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First(); tunInbound.settings.gateway = [address]; - tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"]; + // Route both families into the tunnel regardless of EnableIPv6Address. That option only + // controls whether the interface gets an IPv6 address; leaving ::/0 out of the routing + // table makes IPv6 follow the system default route and bypass the tunnel entirely. + tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0", "::/0"]; if (_config.TunModeItem.EnableIPv6Address == true) { var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First(); tunInbound.settings.gateway.Add(address6); - tunInbound.settings.autoSystemRoutingTable.Add("::/0"); } var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx(); @@ -118,15 +120,8 @@ public partial class CoreConfigV2rayService includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList(); includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList(); - if (_config.TunModeItem.EnableIPv6Address) - { - tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()) - .Concat(includeListV6.Select(x => x.ToString())).ToList(); - } - else - { - tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()).ToList(); - } + tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()) + .Concat(includeListV6.Select(x => x.ToString())).ToList(); } _coreConfig.inbounds.Add(tunInbound);