fix(tun): automatically exclude proxy node IPs and resolved domains from TUN routing to prevent loops (#9974) (#10012)

This commit is contained in:
Mangoo
2026-08-22 09:49:27 +03:00
committed by GitHub
parent 74bd28bbd0
commit 2be63d1655
2 changed files with 173 additions and 12 deletions
@@ -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();
}
}
}