mirror of
https://github.com/2dust/v2rayN.git
synced 2026-09-14 02:12:06 +03:00
Skip the TUN IPv6 route when the host has no global IPv6 address (#10080)
#9930 made the Xray TUN inbound always request ::/0 in autoSystemRoutingTable so that IPv6 stops bypassing the tunnel. That only helps a host which actually holds a globally routable IPv6 address. On any other host it does harm. With IPv6 disabled the TUN device gets no IPv6 address at all, the kernel rejects the route with EACCES and the whole inbound fails to start: Failed to start: app/proxyman/inbound: failed to start proxy > proxy/tun: failed to add system route ::/0 > permission denied With IPv6 enabled but no global address the route is accepted and the host gains an IPv6 default route it cannot use. The TUN completes the TCP handshake locally before dialing the outbound, so IPv6 destinations start to look reachable and get picked, and the connection then dies at the outbound instead of failing fast (#10051). Neither host has IPv6 traffic that could bypass the tunnel, so ::/0 buys them nothing. Detect a global IPv6 address once while building the config context and drop ::/0 when there is none. Link-local and unique local addresses do not count: they never reach the IPv6 internet. Co-authored-by: liuclare <177657698+liuclare@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -205,7 +205,8 @@ internal static class CoreConfigTestFactory
|
||||
return node;
|
||||
}
|
||||
|
||||
public static CoreConfigContext CreateContext(Config config, ProfileItem node, ECoreType runCoreType)
|
||||
public static CoreConfigContext CreateContext(Config config, ProfileItem node, ECoreType runCoreType,
|
||||
bool hasGlobalIPv6Address = true)
|
||||
{
|
||||
return new CoreConfigContext
|
||||
{
|
||||
@@ -226,6 +227,7 @@ internal static class CoreConfigTestFactory
|
||||
FullConfigTemplate = null,
|
||||
IsTunEnabled = config.TunModeItem.EnableTun,
|
||||
ProtectDomainList = [],
|
||||
HasGlobalIPv6Address = hasGlobalIPv6Address,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -588,6 +588,53 @@ public class CoreConfigV2rayServiceTests
|
||||
await tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(true)]
|
||||
[Arguments(false)]
|
||||
public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6(bool enableIPv6Address)
|
||||
{
|
||||
// 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);
|
||||
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<V2rayConfig>(result.Data!.ToString())!;
|
||||
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||
|
||||
await tunInbound.Should().NotBeNull();
|
||||
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0");
|
||||
var ipv6Routes = tunInbound.settings.autoSystemRoutingTable!.Where(x => x.Contains(':')).ToList();
|
||||
await ipv6Routes.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldSkipIPv6RangesWithoutGlobalIPv6()
|
||||
{
|
||||
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, hasGlobalIPv6Address: false);
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||
|
||||
await tunInbound.Should().NotBeNull();
|
||||
await tunInbound!.settings.autoSystemRoutingTable.Should().NotBeEmpty();
|
||||
var ipv6Routes = tunInbound.settings.autoSystemRoutingTable!.Where(x => x.Contains(':')).ToList();
|
||||
await ipv6Routes.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges()
|
||||
{
|
||||
|
||||
@@ -815,6 +815,40 @@ public class Utils
|
||||
.Any(ni => ni.Name.Equals(inInterfaceName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the host holds a globally routable IPv6 address, that is one inside 2000::/3.
|
||||
/// Link-local and unique local addresses are excluded: they never reach the IPv6 internet,
|
||||
/// so a host holding only those has no IPv6 traffic that could bypass the tunnel, and no
|
||||
/// IPv6 path that traffic sent into the tunnel could come back out of.
|
||||
/// </summary>
|
||||
public static bool HasGlobalIPv6Address()
|
||||
{
|
||||
try
|
||||
{
|
||||
return NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(ni => ni.OperationalStatus == OperationalStatus.Up
|
||||
&& ni.NetworkInterfaceType != NetworkInterfaceType.Loopback)
|
||||
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
|
||||
.Any(ua => IsGlobalUnicastIPv6(ua.Address));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsGlobalUnicastIPv6(IPAddress address)
|
||||
{
|
||||
if (address.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2000::/3 is the only range currently assigned for global unicast, which leaves out
|
||||
// ::1, fe80::/10, fc00::/7 and ff00::/8 in a single test.
|
||||
return (address.GetAddressBytes()[0] & 0xE0) == 0x20;
|
||||
}
|
||||
|
||||
#endregion Speed Test
|
||||
|
||||
#region Miscellaneous
|
||||
|
||||
@@ -49,6 +49,7 @@ public class CoreConfigContextBuilder
|
||||
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
|
||||
IsWindows = Utils.IsWindows(),
|
||||
IsMacOS = Utils.IsMacOS(),
|
||||
HasGlobalIPv6Address = Utils.HasGlobalIPv6Address(),
|
||||
ProtectCoreTypeList = config.TunModeItem.EnableTun ? [ECoreType.Xray, ECoreType.sing_box] : []
|
||||
};
|
||||
var validatorResult = NodeValidatorResult.Empty();
|
||||
|
||||
@@ -25,6 +25,10 @@ public record CoreConfigContext
|
||||
public bool IsWindows { get; init; }
|
||||
public bool IsMacOS { get; init; }
|
||||
|
||||
// Defaults to true so that a context built without this flag keeps routing IPv6 into the
|
||||
// tunnel; only a positive detection of the host having no global IPv6 address turns it off.
|
||||
public bool HasGlobalIPv6Address { get; init; } = true;
|
||||
|
||||
// Generation Context
|
||||
public Dictionary<object, string> CustomOutboundMap { get; init; } = new();
|
||||
}
|
||||
|
||||
@@ -70,7 +70,11 @@ public partial class CoreConfigV2rayService
|
||||
// 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"];
|
||||
// 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
|
||||
? ["0.0.0.0/0", "::/0"]
|
||||
: ["0.0.0.0/0"];
|
||||
if (_config.TunModeItem.EnableIPv6Address == true)
|
||||
{
|
||||
var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
|
||||
@@ -95,7 +99,9 @@ public partial class CoreConfigV2rayService
|
||||
.Where(x => x != null).ToList();
|
||||
|
||||
var includeList = new List<IPNetwork2> { wholeInternet };
|
||||
var includeListV6 = new List<IPNetwork2> { wholeInternetV6 };
|
||||
var includeListV6 = context.HasGlobalIPv6Address
|
||||
? new List<IPNetwork2> { wholeInternetV6 }
|
||||
: new List<IPNetwork2>();
|
||||
|
||||
foreach (var exclude in excludeList)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user