diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs index e352d0c5..65bda048 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs @@ -587,13 +587,11 @@ public class CoreConfigV2rayServiceTests } [Test] - [Arguments(true)] - [Arguments(false)] - public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6(bool enableIPv6Address) + public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6() { // A host without a global IPv6 address has no IPv6 traffic that could bypass the tunnel, // while ::/0 would pull IPv6 attempts into a tunnel they cannot leave. - var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address); + var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address: false); CoreConfigTestFactory.BindAppManagerConfig(config); var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); @@ -611,6 +609,26 @@ public class CoreConfigV2rayServiceTests await ipv6Routes.Should().BeEmpty(); } + [Test] + public async Task GenerateClientConfigContent_Tun_ShouldRouteIPv6WhenEnabledWithoutGlobalIPv6() + { + // Hosts behind NAT66 hold only ULA addresses, so EnableIPv6Address must still route ::/0. + var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address: true); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray, hasGlobalIPv6Address: false); + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + await result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun"); + + await tunInbound.Should().NotBeNull(); + await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("::/0"); + } + [Test] public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldSkipIPv6RangesWithoutGlobalIPv6() { diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs index cfed9aaa..3035bbce 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs @@ -67,12 +67,10 @@ public partial class CoreConfigV2rayService var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First(); tunInbound.settings.gateway = [address]; - // 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. - // A host without a global IPv6 address is the exception: it has nothing to leak, - // and IPv6 sent into the tunnel would have no way back out. - tunInbound.settings.autoSystemRoutingTable = context.HasGlobalIPv6Address + // Without ::/0, IPv6 bypasses the tunnel. Skip it only when the host has no global IPv6 + // address and EnableIPv6Address is off; NAT66 hosts have only ULA addresses. + var routeIPv6 = _config.TunModeItem.EnableIPv6Address || context.HasGlobalIPv6Address; + tunInbound.settings.autoSystemRoutingTable = routeIPv6 ? ["0.0.0.0/0", "::/0"] : ["0.0.0.0/0"]; if (_config.TunModeItem.EnableIPv6Address == true) @@ -108,7 +106,7 @@ public partial class CoreConfigV2rayService .Where(x => x != null).ToList(); var includeList = new List { wholeInternet }; - var includeListV6 = context.HasGlobalIPv6Address + var includeListV6 = routeIPv6 ? new List { wholeInternetV6 } : new List();