mirror of
https://github.com/2dust/v2rayN.git
synced 2026-10-29 00:25:00 +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);
|
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)
|
private static string NewId(string prefix)
|
||||||
{
|
{
|
||||||
return $"{prefix}-{Guid.NewGuid():N}";
|
return $"{prefix}-{Guid.NewGuid():N}";
|
||||||
@@ -96,12 +186,22 @@ public class CoreConfigContextBuilderTests
|
|||||||
|| message.Contains("циклическую зависимость", StringComparison.OrdinalIgnoreCase);
|
|| message.Contains("циклическую зависимость", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly SemaphoreSlim _dbLock = new(1, 1);
|
||||||
|
|
||||||
private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)
|
private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)
|
||||||
{
|
{
|
||||||
SQLiteHelper.Instance.CreateTable<ProfileItem>();
|
await _dbLock.WaitAsync();
|
||||||
foreach (var profile in profiles)
|
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 appConfig = JsonUtils.DeepCopy(config);
|
||||||
var routeExcludeAddressList = new List<string>();
|
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);
|
try
|
||||||
routeExcludeAddressList.Add(addr);
|
{
|
||||||
}
|
IPNetwork2.Parse(addr);
|
||||||
catch
|
routeExcludeAddressList.Add(addr);
|
||||||
{
|
}
|
||||||
validatorResult.Warnings.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, 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;
|
appConfig.TunModeItem.RouteExcludeAddress = routeExcludeAddressList;
|
||||||
context = context with { AppConfig = appConfig };
|
context = context with { AppConfig = appConfig };
|
||||||
}
|
}
|
||||||
@@ -505,4 +557,13 @@ public class CoreConfigContextBuilder
|
|||||||
context.AllProxiesMap[node.IndexId] = node;
|
context.AllProxiesMap[node.IndexId] = node;
|
||||||
return childNodeValidatorResult;
|
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