mirror of
https://github.com/2dust/v2rayN.git
synced 2026-08-22 23:22:04 +03:00
fix(tun): automatically exclude proxy node IPs and resolved domains from TUN routing to prevent loops (#9974) (#10012)
This commit is contained in:
@@ -83,6 +83,96 @@ public class CoreConfigContextBuilderTests
|
||||
await groupA.GetProtocolExtra().ChildItems.Should().BeEqualTo(leaf.IndexId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Build_WhenTunEnabled_ShouldAutoExcludeProxyServerAddress()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
config.TunModeItem.EnableTun = true;
|
||||
config.TunModeItem.RouteExcludeAddress = ["10.0.0.0/8"];
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "vmess-node");
|
||||
node.Address = "1.2.3.4";
|
||||
await UpsertProfilesAsync(node);
|
||||
|
||||
var result = await CoreConfigContextBuilder.Build(config, node);
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
await result.Context.AppConfig.TunModeItem.RouteExcludeAddress.Should().Contain("10.0.0.0/8");
|
||||
await result.Context.AppConfig.TunModeItem.RouteExcludeAddress.Should().Contain("1.2.3.4/32");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Build_WhenTunEnabled_ShouldAutoExcludeProxyServerAddress_IPv6WithBrackets()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
config.TunModeItem.EnableTun = true;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "vmess-v6");
|
||||
node.Address = "[2001:db8::1]";
|
||||
await UpsertProfilesAsync(node);
|
||||
|
||||
var result = await CoreConfigContextBuilder.Build(config, node);
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
await result.Context.AppConfig.TunModeItem.RouteExcludeAddress.Should().Contain("2001:db8::1/128");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Build_WhenTunEnabled_ShouldNotExcludeLoopbackAddress()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
config.TunModeItem.EnableTun = true;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "vmess-loopback");
|
||||
node.Address = "127.0.0.1";
|
||||
await UpsertProfilesAsync(node);
|
||||
|
||||
var result = await CoreConfigContextBuilder.Build(config, node);
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
await result.Context.AppConfig.TunModeItem.RouteExcludeAddress.Should().NotContain("127.0.0.1/32");
|
||||
await result.Context.AppConfig.TunModeItem.RouteExcludeAddress.Should().NotContain("127.0.0.1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Build_WhenTunEnabled_ShouldNotExcludeAnyOrNoneAddress()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
config.TunModeItem.EnableTun = true;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "vmess-any");
|
||||
node.Address = "0.0.0.0";
|
||||
await UpsertProfilesAsync(node);
|
||||
|
||||
var result = await CoreConfigContextBuilder.Build(config, node);
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
await result.Context.AppConfig.TunModeItem.RouteExcludeAddress.Should().NotContain("0.0.0.0/32");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Build_WhenTunEnabled_WithDomainNode_ShouldExcludeResolvedIPs()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
config.TunModeItem.EnableTun = true;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "vmess-domain");
|
||||
node.Address = "one.one.one.one";
|
||||
await UpsertProfilesAsync(node);
|
||||
|
||||
var result = await CoreConfigContextBuilder.Build(config, node);
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
await result.Context.AppConfig.TunModeItem.RouteExcludeAddress.Should().NotBeNull();
|
||||
await result.Context.AppConfig.TunModeItem.RouteExcludeAddress!.Count.Should().BeGreaterThan(0);
|
||||
await result.Context.AppConfig.TunModeItem.RouteExcludeAddress.Should().Contain(x => x.StartsWith("1.1.1.1") || x.StartsWith("1.0.0.1") || x.Contains(':'));
|
||||
}
|
||||
|
||||
private static string NewId(string prefix)
|
||||
{
|
||||
return $"{prefix}-{Guid.NewGuid():N}";
|
||||
@@ -96,12 +186,22 @@ public class CoreConfigContextBuilderTests
|
||||
|| message.Contains("циклическую зависимость", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static readonly SemaphoreSlim _dbLock = new(1, 1);
|
||||
|
||||
private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)
|
||||
{
|
||||
SQLiteHelper.Instance.CreateTable<ProfileItem>();
|
||||
foreach (var profile in profiles)
|
||||
await _dbLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await SQLiteHelper.Instance.ReplaceAsync(profile);
|
||||
SQLiteHelper.Instance.CreateTable<ProfileItem>();
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
await SQLiteHelper.Instance.ReplaceAsync(profile);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dbLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,22 +96,74 @@ public class CoreConfigContextBuilder
|
||||
}
|
||||
}
|
||||
|
||||
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
|
||||
if (context.IsTunEnabled)
|
||||
{
|
||||
var appConfig = JsonUtils.DeepCopy(config);
|
||||
var routeExcludeAddressList = new List<string>();
|
||||
foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress)
|
||||
if (context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
|
||||
{
|
||||
try
|
||||
foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress)
|
||||
{
|
||||
IPNetwork2.Parse(addr);
|
||||
routeExcludeAddressList.Add(addr);
|
||||
}
|
||||
catch
|
||||
{
|
||||
validatorResult.Warnings.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr));
|
||||
try
|
||||
{
|
||||
IPNetwork2.Parse(addr);
|
||||
routeExcludeAddressList.Add(addr);
|
||||
}
|
||||
catch
|
||||
{
|
||||
validatorResult.Warnings.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude proxy server IP addresses from TUN to prevent routing loops
|
||||
var allNodes = context.AllProxiesMap.Values
|
||||
.Append(context.Node)
|
||||
.Where(n => n != null && !n.ConfigType.IsGroupType() && n.ConfigType != EConfigType.Outbound);
|
||||
|
||||
var uniqueAddresses = allNodes
|
||||
.Select(n => n.Address?.Trim().Trim('[', ']'))
|
||||
.Where(a => !string.IsNullOrEmpty(a))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var pAddr in uniqueAddresses)
|
||||
{
|
||||
if (IPAddress.TryParse(pAddr, out var ipAddr))
|
||||
{
|
||||
if (IsValidExcludableIP(ipAddr))
|
||||
{
|
||||
var cidr = ipAddr.AddressFamily == AddressFamily.InterNetworkV6 ? $"{ipAddr}/128" : $"{ipAddr}/32";
|
||||
if (!routeExcludeAddressList.Contains(cidr))
|
||||
{
|
||||
routeExcludeAddressList.Add(cidr);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Utils.IsDomain(pAddr))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(2000);
|
||||
var addresses = await Dns.GetHostAddressesAsync(pAddr, cts.Token);
|
||||
foreach (var addr in addresses)
|
||||
{
|
||||
if (IsValidExcludableIP(addr))
|
||||
{
|
||||
var cidr = addr.AddressFamily == AddressFamily.InterNetworkV6 ? $"{addr}/128" : $"{addr}/32";
|
||||
if (!routeExcludeAddressList.Contains(cidr))
|
||||
{
|
||||
routeExcludeAddressList.Add(cidr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore DNS resolution failures at config build time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appConfig.TunModeItem.RouteExcludeAddress = routeExcludeAddressList;
|
||||
context = context with { AppConfig = appConfig };
|
||||
}
|
||||
@@ -505,4 +557,13 @@ public class CoreConfigContextBuilder
|
||||
context.AllProxiesMap[node.IndexId] = node;
|
||||
return childNodeValidatorResult;
|
||||
}
|
||||
|
||||
private static bool IsValidExcludableIP(IPAddress ipAddr)
|
||||
{
|
||||
return !IPAddress.IsLoopback(ipAddr)
|
||||
&& !ipAddr.Equals(IPAddress.Any)
|
||||
&& !ipAddr.Equals(IPAddress.IPv6Any)
|
||||
&& !ipAddr.Equals(IPAddress.None)
|
||||
&& !ipAddr.Equals(IPAddress.IPv6None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user