fix(xray): keep direct DNS ahead of catch-all proxy rules (#10599)

* fix(xray): keep direct DNS ahead of catch-all proxy rules

* fix(xray): cover full nonzero port range in DNS routing
This commit is contained in:
Desaichk
2026-11-15 11:15:10 +03:00
committed by GitHub
parent 4e40d5f5f7
commit 9f599c60a7
2 changed files with 107 additions and 1 deletions
@@ -2,6 +2,98 @@ namespace ServiceLib.Tests.CoreConfig.V2ray;
public class CoreConfigV2rayServiceTests
{
[Test]
[Arguments(false, "0-65535", null, Global.ProxyTag)]
[Arguments(false, "1-65535", null, Global.ProxyTag)]
[Arguments(true, "0-65535", null, Global.ProxyTag)]
[Arguments(true, null, "tcp,udp", Global.ProxyTag)]
[Arguments(false, null, null, Global.ProxyTag)]
[Arguments(false, "0-65535", null, Global.BlockTag)]
public async Task DirectDnsRule_ShouldPrecedeCatchAllAndPreserveExplicitRules(bool tun, string? port, string? network, string outbound)
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
config.TunModeItem.EnableTun = tun;
config.SimpleDNSItem.DirectDNS = "8.8.8.8";
config.SimpleDNSItem.RemoteDNS = "1.1.1.1";
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray);
var userRules = new List<RulesItem>
{
new() { RuleType = ERuleType.DNS, Domain = ["domain:direct.example"], OutboundTag = Global.DirectTag },
new() { InboundTag = ["direct-dns-1"], Ip = ["8.8.8.8"], Port = "53", OutboundTag = Global.BlockTag },
new() { Port = "443", Network = "tcp", OutboundTag = Global.ProxyTag },
};
if (port != null || network != null)
{
userRules.Add(new() { Port = port, Network = network, OutboundTag = outbound });
}
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with
{
RoutingItem = new RoutingItem { RuleSet = JsonUtils.Serialize(userRules), DomainStrategy = Global.IPOnDemand }
};
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue();
var rules = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!.routing.rules;
var direct = rules.FindIndex(r => r.outboundTag == Global.DirectTag && r.inboundTag?.Contains("direct-dns-1") == true);
var explicitBlock = rules.FindIndex(r => r.outboundTag == Global.BlockTag && r.inboundTag?.Contains("direct-dns-1") == true);
var https = rules.FindIndex(r => r.port == "443" && r.network == "tcp" && r.outboundTag == Global.ProxyTag);
await (explicitBlock >= 0 && https > explicitBlock && direct > https).Should().BeTrue();
if (port != null || network != null)
{
var catchAll = rules.FindIndex(r => r.port == port && r.network == network && r.inboundTag == null && r.outboundTag == outbound);
await (catchAll >= 0 && (outbound == Global.ProxyTag ? catchAll > direct : catchAll < direct)).Should().BeTrue();
}
if (tun)
{
var intercept = rules.FindIndex(r => r.inboundTag?.Contains("tun") == true && r.port == "53" && r.outboundTag == Global.DnsOutboundTag);
await (intercept >= 0 && intercept < direct).Should().BeTrue();
}
await rules.Should().Contain(r => r.inboundTag != null && r.inboundTag.Contains(Global.DnsTag) && r.outboundTag == Global.ProxyTag);
}
[Test]
[Arguments("0-65535", null)]
[Arguments("1-65535", null)]
[Arguments(null, "udp,tcp")]
public async Task DirectDnsRule_ShouldPrecedeCatchAllBalancer(string? port, string? network)
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
config.SimpleDNSItem.DirectDNS = "8.8.8.8,https://8.8.4.4/dns-query";
config.SimpleDNSItem.RemoteDNS = "1.1.1.1";
CoreConfigTestFactory.BindAppManagerConfig(config);
var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n1", "node-1");
var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n2", "node-2");
var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "g1", "group", [n1.IndexId, n2.IndexId]);
var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.Xray) with
{
RoutingItem = new RoutingItem
{
RuleSet = JsonUtils.Serialize(new List<RulesItem>
{
new() { RuleType = ERuleType.DNS, Domain = ["domain:direct.example"], OutboundTag = Global.DirectTag },
new() { Port = port, Network = network, OutboundTag = Global.ProxyTag },
}),
DomainStrategy = Global.AsIs,
},
};
context.AllProxiesMap[n1.IndexId] = n1;
context.AllProxiesMap[n2.IndexId] = n2;
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue();
var rules = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!.routing.rules;
var direct = rules.FindIndex(r => r.outboundTag == Global.DirectTag && r.inboundTag?.Contains("direct-dns-1") == true);
var catchAll = rules.FindIndex(r => r.port == port && r.network == network && r.inboundTag == null
&& r.balancerTag == Global.ProxyTag + Global.BalancerTagSuffix);
await (direct >= 0 && catchAll > direct).Should().BeTrue();
await rules[direct].inboundTag.Should().Contain("direct-dns-2");
await rules.Should().Contain(r => r.inboundTag != null && r.inboundTag.Contains(Global.DnsTag)
&& r.balancerTag == Global.ProxyTag + Global.BalancerTagSuffix);
}
[Test]
[Arguments("demo-seed", "srtp", 2)]
[Arguments(null, "srtp", 2)]
@@ -99,7 +99,9 @@ public partial class CoreConfigV2rayService
.ToList();
if (directDnsTags.Count > 0)
{
_coreConfig.routing.rules.Add(new()
var rules = _coreConfig.routing.rules;
var catchAllIndex = rules.FindIndex(IsDnsRoutingCatchAll);
rules.Insert(catchAllIndex < 0 ? rules.Count : catchAllIndex, new()
{
type = "field",
inboundTag = directDnsTags,
@@ -125,6 +127,18 @@ public partial class CoreConfigV2rayService
}
}
private static bool IsDnsRoutingCatchAll(RulesItem4Ray rule)
{
return (rule.outboundTag == Global.ProxyTag || rule.balancerTag == Global.ProxyTag + Global.BalancerTagSuffix)
&& rule.inboundTag is not { Count: > 0 }
&& rule.domain is not { Count: > 0 }
&& rule.ip is not { Count: > 0 }
&& rule.protocol is not { Count: > 0 }
&& rule.process is not { Count: > 0 }
&& rule.port is null or "0-65535" or "1-65535"
&& rule.network is null or "tcp,udp" or "udp,tcp";
}
private void GenFakeDns()
{
var fakeipRange = _config.SimpleDNSItem.FakeIPRange.IsNullOrEmpty() ? Global.FakeIPRanges.First() : _config.SimpleDNSItem.FakeIPRange;