diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs index 1055a248..a0825357 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs @@ -378,50 +378,6 @@ public class CoreConfigSingboxServiceTests await hasRouteRule.Should().BeTrue(); } - [Test] - public async Task GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule() - { - var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs( - ECoreType.sing_box, - "192.168.0.0/16,geoip:cn"); - CoreConfigTestFactory.BindAppManagerConfig(config); - - var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); - var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with - { - RoutingItem = new RoutingItem - { - Id = "r-dns-direct-expected", - Remarks = "dns-direct-expected", - RuleSet = JsonUtils.Serialize(new List - { - new() - { - Enabled = true, - RuleType = ERuleType.DNS, - OutboundTag = Global.DirectTag, - Domain = ["geosite:cn"], - } - }), - DomainStrategy = Global.AsIs, - DomainStrategy4Singbox = string.Empty, - } - }; - - var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); - - await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}"); - var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; - - var hasExpectedRule = cfg.dns.rules?.Any(r => - r.server == Global.SingboxDirectDNSTag - && r.ip_cidr?.Contains("192.168.0.0/16") == true - && r.rule_set?.Contains("geosite-cn") == true - && r.rule_set?.Contains("geoip-cn") == true) ?? false; - - await hasExpectedRule.Should().BeTrue(); - } - [Test] public async Task GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver() { @@ -544,14 +500,18 @@ public class CoreConfigSingboxServiceTests Remarks = "dns-direct-variant", RuleSet = JsonUtils.Serialize(new List { + new() + { + Enabled = true, RuleType = ERuleType.DNS, OutboundTag = Global.ProxyTag, Domain = ["geosite:google"], + }, new() { Enabled = true, RuleType = ERuleType.DNS, OutboundTag = Global.DirectTag, Domain = [domainTag], - } + }, }), DomainStrategy = Global.AsIs, DomainStrategy4Singbox = string.Empty, - } + }, }; var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); @@ -559,11 +519,14 @@ public class CoreConfigSingboxServiceTests await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}"); var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; - var hasExpectedRule = cfg.dns.rules?.Any(r => - r.server == Global.SingboxDirectDNSTag - && r.ip_cidr?.Contains("192.168.0.0/16") == true - && r.rule_set?.Contains(expectedRuleSetTag) == true - && r.rule_set?.Contains("geoip-cn") == true) ?? false; + var hasExpectedRule = (cfg.dns.rules?.Any(r => + r.ip_cidr?.Contains("192.168.0.0/16") == true + && r.action == "respond" + && r.rule_set?.Contains("geoip-cn") == true) ?? false) + && (cfg.dns.rules?.Any(r => + r.server == Global.SingboxDirectDNSTag + && r.rule_set?.Contains(expectedRuleSetTag) == true + && r.action == "evaluate") ?? false); await hasExpectedRule.Should().BeTrue(); } @@ -632,6 +595,60 @@ public class CoreConfigSingboxServiceTests await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct)); } + [Test] + [Arguments(true)] + [Arguments(false)] + public async Task GenerateClientConfigContent_MultiDnsServers_ShouldIncludeAllServers(bool parallelQuery) + { + var config = + CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + config.SimpleDNSItem.ParallelQuery = parallelQuery; + config.SimpleDNSItem.DirectDNS = "1.1.1.1,2.2.2.2"; + config.SimpleDNSItem.RemoteDNS = "3.3.3.3,4.4.4.4"; + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + RoutingItem = new RoutingItem + { + Id = "r-dns-direct-variant", + Remarks = "dns-direct-variant", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.DNS, + OutboundTag = Global.ProxyTag, + Domain = ["geosite:google"], + }, + new() + { + Enabled = true, + RuleType = ERuleType.DNS, + OutboundTag = Global.DirectTag, + Domain = ["geosite:cn"], + }, + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + }, + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + await cfg.dns.servers.Where(s => s.tag.StartsWith(Global.SingboxDirectDNSTagPrefix)).Should().HaveCount(2); + await cfg.dns.servers.Where(s => s.tag.StartsWith(Global.SingboxRemoteDNSTagPrefix)).Should().HaveCount(2); + if (parallelQuery) + { + await cfg.dns.rules.Should().Contain(r => r.race == true); + } + } + [Test] public async Task GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl() { diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index 55df7714..7724cb37 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -97,11 +97,16 @@ public class Global public const string StringFalse = "false"; public const int SqliteMaxBatchSize = 10000; - public const string SingboxDirectDNSTag = "direct_dns"; - public const string SingboxRemoteDNSTag = "remote_dns"; - public const string SingboxLocalDNSTag = "local_local"; - public const string SingboxHostsDNSTag = "hosts_dns"; - public const string SingboxFakeDNSTag = "fake_dns"; + public const string SingboxDirectDNSTagPrefix = "direct-dns-"; + public const string SingboxRemoteDNSTagPrefix = "remote-dns-"; + public const string SingboxDirectDNSTag = "direct-dns-1"; + public const string SingboxRemoteDNSTag = "remote-dns-1"; + public const string SingboxDirectDNSTagTemplate = "direct-dns-{0}"; + public const string SingboxRemoteDNSTagTemplate = "remote-dns-{0}"; + public const string SingboxLocalDNSTag = "local-local"; + public const string SingboxHostsDNSTag = "hosts-dns"; + public const string SingboxFakeDNSTag = "fake-dns"; + public const string SingboxSrsDownloadHttpClientTag = "srs-download-http-client"; public const int Hysteria2DefaultHopInt = 30; public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset"; diff --git a/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs index a835b15f..806af4b4 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs @@ -8,6 +8,7 @@ public class SingboxConfig public List outbounds { get; set; } public List? endpoints { get; set; } public Route4Sbox route { get; set; } + public List? http_clients { get; set; } public Experimental4Sbox? experimental { get; set; } } @@ -24,6 +25,7 @@ public class Dns4Sbox public List servers { get; set; } public List rules { get; set; } public string? final { get; set; } + public bool? optimistic { get; set; } public string? strategy { get; set; } public bool? disable_cache { get; set; } public bool? disable_expire { get; set; } @@ -94,6 +96,11 @@ public class Rule4Sbox public bool? rule_set_ip_cidr_match_source { get; set; } public bool? rule_set_ip_cidr_accept_empty { get; set; } public bool? tls_record_fragment { get; set; } + public string? preferred_by { get; set; } + public string? match_response { get; set; } // or bool + public string? tag { get; set; } + public bool? race { get; set; } + public bool? speculative { get; set; } } [Serializable] @@ -108,7 +115,6 @@ public class Inbound4Sbox public int? mtu { get; set; } public bool? auto_route { get; set; } public bool? strict_route { get; set; } - public bool? endpoint_independent_nat { get; set; } public string? stack { get; set; } public List users { get; set; } public List? route_exclude_address { get; set; } @@ -318,10 +324,12 @@ public class Ruleset4Sbox public string? format { get; set; } public string? path { get; set; } public string? url { get; set; } - public string? download_detour { get; set; } + public string? http_client { get; set; } public string? update_interval { get; set; } } +public class HttpClient4Sbox : BaseServer4Sbox; + public abstract class DialFields4Sbox { public string? detour { get; set; } diff --git a/v2rayN/ServiceLib/Sample/dns_singbox_normal b/v2rayN/ServiceLib/Sample/dns_singbox_normal index 2df81395..49ab8b81 100644 --- a/v2rayN/ServiceLib/Sample/dns_singbox_normal +++ b/v2rayN/ServiceLib/Sample/dns_singbox_normal @@ -17,17 +17,15 @@ "rule_set": [ "geosite-google" ], - "server": "remote", - "strategy": "prefer_ipv4" + "server": "remote" }, { "rule_set": [ "geosite-cn" ], - "server": "local", - "strategy": "prefer_ipv4" + "server": "local" } ], "final": "remote", "strategy": "prefer_ipv4" -} +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/tun_singbox_dns b/v2rayN/ServiceLib/Sample/tun_singbox_dns index 4f91c6ff..49ab8b81 100644 --- a/v2rayN/ServiceLib/Sample/tun_singbox_dns +++ b/v2rayN/ServiceLib/Sample/tun_singbox_dns @@ -17,15 +17,13 @@ "rule_set": [ "geosite-google" ], - "server": "remote", - "strategy": "prefer_ipv4" + "server": "remote" }, { "rule_set": [ "geosite-cn" ], - "server": "local", - "strategy": "prefer_ipv4" + "server": "local" } ], "final": "remote", diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index ceffc0f1..e05e6bba 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -17,36 +17,11 @@ public partial class CoreConfigSingboxService GenDnsRules(); _coreConfig.dns ??= new Dns4Sbox(); - _coreConfig.dns.independent_cache = true; + _coreConfig.dns.optimistic = context.SimpleDnsItem.ServeStale == true ? true : null; // final dns - var routing = context.RoutingItem; - var useDirectDns = false; - if (routing != null) - { - var rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; - - if (rules?.LastOrDefault() is { OutboundTag: Global.DirectTag } lastRule) - { - var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0; - var noProcess = lastRule.Process == null || lastRule.Process.Count == 0; - var isAnyIp = lastRule.Ip == null || lastRule.Ip.Count == 0 || lastRule.Ip.Contains("0.0.0.0/0"); - var isAnyPort = string.IsNullOrEmpty(lastRule.Port) || lastRule.Port == "0-65535"; - var isAnyNetwork = string.IsNullOrEmpty(lastRule.Network) || lastRule.Network == "tcp,udp"; - useDirectDns = noDomain && noProcess && isAnyIp && isAnyPort && isAnyNetwork; - } - } + var useDirectDns = UseDirectDns(); _coreConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag; - var simpleDnsItem = context.SimpleDnsItem; - if ((!useDirectDns) && simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp == false) - { - _coreConfig.dns.rules.Add(new() - { - server = Global.SingboxFakeDNSTag, - query_type = new List { 1, 28 }, // A and AAAA - rewrite_ttl = 1, - }); - } } catch (Exception ex) { @@ -59,14 +34,24 @@ public partial class CoreConfigSingboxService var simpleDnsItem = context.SimpleDnsItem; var finalDns = GenBootstrapDns(); - var directDns = ParseDnsAddress(simpleDnsItem.DirectDNS ?? Global.DomainDirectDNSAddress.First()); - directDns.tag = Global.SingboxDirectDNSTag; - directDns.domain_resolver = Global.SingboxLocalDNSTag; + var directDnsList = ParseDnsAddresses(simpleDnsItem.DirectDNS ?? Global.DomainDirectDNSAddress.First()); + for (var i = 0; i < directDnsList.Count; i++) + { + var directDns = directDnsList[i]; + var tag = string.Format(Global.SingboxDirectDNSTagTemplate, i + 1); + directDns.tag = tag; + directDns.domain_resolver = Global.SingboxLocalDNSTag; + } - var remoteDns = ParseDnsAddress(simpleDnsItem.RemoteDNS ?? Global.DomainRemoteDNSAddress.First()); - remoteDns.tag = Global.SingboxRemoteDNSTag; - remoteDns.detour = Global.ProxyTag; - remoteDns.domain_resolver = Global.SingboxLocalDNSTag; + var remoteDnsList = ParseDnsAddresses(simpleDnsItem.RemoteDNS ?? Global.DomainRemoteDNSAddress.First()); + for (var i = 0; i < remoteDnsList.Count; i++) + { + var remoteDns = remoteDnsList[i]; + var tag = string.Format(Global.SingboxRemoteDNSTagTemplate, i + 1); + remoteDns.tag = tag; + remoteDns.detour = Global.ProxyTag; + remoteDns.domain_resolver = Global.SingboxLocalDNSTag; + } var hostsDns = new Server4Sbox { @@ -118,26 +103,28 @@ public partial class CoreConfigSingboxService { finalDns.domain_resolver = Global.SingboxHostsDNSTag; } - if (remoteDns.server == host.Key) - { - remoteDns.domain_resolver = Global.SingboxHostsDNSTag; - } - if (directDns.server == host.Key) + foreach (var directDns in directDnsList.Where(directDns => directDns.server == host.Key)) { directDns.domain_resolver = Global.SingboxHostsDNSTag; } + foreach (var remoteDns in remoteDnsList.Where(remoteDns => remoteDns.server == host.Key)) + { + remoteDns.domain_resolver = Global.SingboxHostsDNSTag; + } } _coreConfig.dns ??= new Dns4Sbox(); _coreConfig.dns.servers ??= []; - _coreConfig.dns.servers.Add(remoteDns); - _coreConfig.dns.servers.Add(directDns); + _coreConfig.dns.servers.AddRange(remoteDnsList); + _coreConfig.dns.servers.AddRange(directDnsList); _coreConfig.dns.servers.Add(hostsDns); // fake ip if (simpleDnsItem.FakeIP == true) { - var fakeipRange = simpleDnsItem.FakeIPRange.IsNullOrEmpty() ? Global.FakeIPRanges.First() : simpleDnsItem.FakeIPRange; + var fakeipRange = simpleDnsItem.FakeIPRange.IsNullOrEmpty() + ? Global.FakeIPRanges.First() + : simpleDnsItem.FakeIPRange; var fakeip = new Server4Sbox { tag = Global.SingboxFakeDNSTag, @@ -164,33 +151,33 @@ public partial class CoreConfigSingboxService _coreConfig.dns ??= new Dns4Sbox(); _coreConfig.dns.rules ??= []; - _coreConfig.dns.rules.Add(new() { ip_accept_any = true, server = Global.SingboxHostsDNSTag }); + _coreConfig.dns.rules.Add(new() + { + preferred_by = Global.SingboxHostsDNSTag, + server = Global.SingboxHostsDNSTag, + }); if (context.ProtectDomainList.Count > 0) { _coreConfig.dns.rules.Add(new() { server = Global.SingboxDirectDNSTag, - strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4ProxyDial), domain = context.ProtectDomainList.ToList(), }); } - _coreConfig.dns.rules.AddRange(new[] - { + _coreConfig.dns.rules.AddRange([ new Rule4Sbox { server = Global.SingboxRemoteDNSTag, - strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy), - clash_mode = nameof(ERuleMode.Global) + clash_mode = nameof(ERuleMode.Global), }, new Rule4Sbox { server = Global.SingboxDirectDNSTag, - strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom), - clash_mode = nameof(ERuleMode.Direct) - } - }); + clash_mode = nameof(ERuleMode.Direct), + }, + ]); foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts)) { @@ -199,7 +186,7 @@ public partial class CoreConfigSingboxService { continue; } - var rule = new Rule4Sbox() + var rule = new Rule4Sbox { query_type = [1, 5, 28], // A, CNAME and AAAA action = "predefined", @@ -262,13 +249,14 @@ public partial class CoreConfigSingboxService { query_type = [64, 65], action = "predefined", - rcode = "NOERROR" + rcode = "NOERROR", }); } if (simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp != false) { - var fakeipFilterRule = JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName)); + var fakeipFilterRule = + JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName)); fakeipFilterRule.invert = true; var rule4Fake = new Rule4Sbox { @@ -305,6 +293,11 @@ public partial class CoreConfigSingboxService return; } + var directDnsList = _coreConfig.dns.servers + .Where(s => s.tag?.StartsWith(Global.SingboxDirectDNSTagPrefix) == true).ToList(); + var remoteDnsList = _coreConfig.dns.servers + .Where(s => s.tag?.StartsWith(Global.SingboxRemoteDNSTagPrefix) == true).ToList(); + var rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; var expectedIPCidr = new HashSet(); var expectedIPsRegions = new HashSet(); @@ -312,8 +305,7 @@ public partial class CoreConfigSingboxService if (!string.IsNullOrEmpty(simpleDnsItem?.DirectExpectedIPs)) { - var ipItems = simpleDnsItem.DirectExpectedIPs - .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) + var ipItems = (Utils.String2List(simpleDnsItem.DirectExpectedIPs) ?? []) .Select(s => s.Trim()) .Where(s => !string.IsNullOrEmpty(s)) .ToHashSet(); @@ -359,35 +351,60 @@ public partial class CoreConfigSingboxService if (item.OutboundTag == Global.DirectTag) { - rule.server = Global.SingboxDirectDNSTag; - rule.strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom); + Rule4Sbox? rule4ExpectedIPs = null; if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0 && !regionName.IsNullOrEmpty()) { - var regionGeosite = rule.geosite.Where(g => g.EndsWith($"-{regionName}", StringComparison.OrdinalIgnoreCase) - || g.EndsWith($"@{regionName}", StringComparison.OrdinalIgnoreCase) - || g == regionName).ToList(); + var regionGeosite = rule.geosite.Where(g => + g.EndsWith($"-{regionName}", StringComparison.OrdinalIgnoreCase) + || g.EndsWith($"@{regionName}", StringComparison.OrdinalIgnoreCase) + || g == regionName).ToList(); if (regionGeosite.Count > 0) { rule.geosite.RemoveAll(regionGeosite.Contains); - var rule4ExpectedIPs = JsonUtils.DeepCopy(rule); + rule4ExpectedIPs = JsonUtils.DeepCopy(rule); rule4ExpectedIPs.geosite = regionGeosite; + } + } + + if (rule.geosite?.Count > 0 || rule.domain?.Count > 0) + { + AddRules(rule, item, directDnsList); + } + + if (rule4ExpectedIPs is not null) + { + var expectedRuleList = BuildMultiRules(rule4ExpectedIPs, item, directDnsList); + foreach (var expectedRule in + expectedRuleList.Where(expectedRule => expectedRule.action == "respond")) + { if (expectedIPsRegions.Count > 0) { - rule4ExpectedIPs.geoip = expectedIPsRegions.ToList(); + expectedRule.geoip = expectedIPsRegions.ToList(); } if (expectedIPCidr.Count > 0) { - rule4ExpectedIPs.ip_cidr = expectedIPCidr.ToList(); + expectedRule.ip_cidr = expectedIPCidr.ToList(); } - _coreConfig.dns.rules.Add(rule4ExpectedIPs); } + _coreConfig.dns.rules.AddRange(expectedRuleList); + var fallbackRemoteRuleList = BuildMultiRules(rule4ExpectedIPs, item, remoteDnsList); + if (expectedRuleList.LastOrDefault()?.race == true) + { + foreach (var fallbackRemoteRule in fallbackRemoteRuleList.Where(fallbackRemoteRule => + fallbackRemoteRule.action == "evaluate")) + { + fallbackRemoteRule.speculative = true; + } + } + _coreConfig.dns.rules.AddRange(fallbackRemoteRuleList); } } else if (item.OutboundTag == Global.BlockTag) { rule.action = "predefined"; rule.rcode = "NXDOMAIN"; + _coreConfig.dns.rules.Add(rule); } else { @@ -395,15 +412,104 @@ public partial class CoreConfigSingboxService { var rule4Fake = JsonUtils.DeepCopy(rule); rule4Fake.server = Global.SingboxFakeDNSTag; - rule4Fake.query_type = new List { 1, 28 }; // A and AAAA + rule4Fake.query_type = new List + { + 1, + 28, + }; // A and AAAA rule4Fake.rewrite_ttl = 1; _coreConfig.dns.rules.Add(rule4Fake); } - rule.server = Global.SingboxRemoteDNSTag; - rule.strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy); + AddRules(rule, item, remoteDnsList); } + } + var useDirectDns = UseDirectDns(); + if ((!useDirectDns) && simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp == false) + { + _coreConfig.dns.rules.Add(new() + { + server = Global.SingboxFakeDNSTag, + query_type = + [ + 1, + 28, + ], // A and AAAA + rewrite_ttl = 1, + }); + } + var tempRuleItem = new RulesItem + { + Id = $"final-{Utils.GetGuid(false)}", + Enabled = true, + }; + AddRules(new(), tempRuleItem, useDirectDns ? directDnsList : remoteDnsList); + return; - _coreConfig.dns.rules.Add(rule); + static List BuildParallelRules(Rule4Sbox rule, RulesItem item, List dnsList) + { + var evaluateRuleList = new List(); + var racingMatchingRuleList = new List(); + foreach (var dnsServer in dnsList) + { + var dnsServerTag = dnsServer.tag; + var evaluateRule = JsonUtils.DeepCopy(rule); + evaluateRule.action = "evaluate"; + // Maybe rule index is better than id? Not sure, but id is unique, so use id for now. + var evaluateTag = $"{dnsServerTag}-{item.Id}"; + evaluateRule.tag = evaluateTag; + evaluateRule.server = dnsServerTag; + evaluateRuleList.Add(evaluateRule); + var racingMatchingRule = new Rule4Sbox + { + match_response = evaluateTag, + race = true, + action = "respond", + }; + racingMatchingRuleList.Add(racingMatchingRule); + } + return [.. evaluateRuleList, .. racingMatchingRuleList]; + } + + static List BuildSerialRules(Rule4Sbox rule, RulesItem item, List dnsList) + { + var ruleList = new List(); + foreach (var dnsServer in dnsList) + { + var dnsServerTag = dnsServer.tag; + var evaluateRule = JsonUtils.DeepCopy(rule); + evaluateRule.action = "evaluate"; + // Maybe rule index is better than id? Not sure, but id is unique, so use id for now. + var evaluateTag = $"{dnsServerTag}-{item.Id}"; + evaluateRule.tag = evaluateTag; + evaluateRule.server = dnsServerTag; + ruleList.Add(evaluateRule); + var racingMatchingRule = new Rule4Sbox + { + match_response = evaluateTag, + action = "respond", + }; + ruleList.Add(racingMatchingRule); + } + return ruleList; + } + + List BuildMultiRules(Rule4Sbox rule, RulesItem item, List dnsList) + { + return simpleDnsItem.ParallelQuery == true ? BuildParallelRules(rule, item, dnsList) : BuildSerialRules(rule, item, dnsList); + } + + void AddRules(Rule4Sbox rule, RulesItem item, List dnsList) + { + if (dnsList.Count == 1) + { + rule.server = dnsList.First().tag; + _coreConfig.dns.rules.Add(rule); + } + else + { + var ruleList = BuildMultiRules(rule, item, dnsList); + _coreConfig.dns.rules.AddRange(ruleList); + } } } @@ -432,11 +538,15 @@ public partial class CoreConfigSingboxService var strDNS = string.Empty; if (context.IsTunEnabled) { - strDNS = string.IsNullOrEmpty(item?.TunDNS) ? EmbedUtils.GetEmbedText(Global.TunSingboxDNSFileName) : item?.TunDNS; + strDNS = string.IsNullOrEmpty(item?.TunDNS) + ? EmbedUtils.GetEmbedText(Global.TunSingboxDNSFileName) + : item?.TunDNS; } else { - strDNS = string.IsNullOrEmpty(item?.NormalDNS) ? EmbedUtils.GetEmbedText(Global.DNSSingboxNormalFileName) : item?.NormalDNS; + strDNS = string.IsNullOrEmpty(item?.NormalDNS) + ? EmbedUtils.GetEmbedText(Global.DNSSingboxNormalFileName) + : item?.NormalDNS; } var dns4Sbox = JsonUtils.Deserialize(strDNS); @@ -464,15 +574,18 @@ public partial class CoreConfigSingboxService dns4Sbox.rules.Insert(0, new() { server = tag, - clash_mode = nameof(ERuleMode.Direct) + clash_mode = nameof(ERuleMode.Direct), }); dns4Sbox.rules.Insert(0, new() { - server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? "remote", - clash_mode = nameof(ERuleMode.Global) + server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? + "remote", + clash_mode = nameof(ERuleMode.Global), }); - var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress; + var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) + ? Global.DomainPureIPDNSAddress.FirstOrDefault() + : dnsItem?.DomainDNSAddress; var localDnsServer = ParseDnsAddress(finalDnsAddress); localDnsServer.tag = tag; @@ -500,9 +613,21 @@ public partial class CoreConfigSingboxService }; } + private static List ParseDnsAddresses(string addresses) + { + var servers = new List(); + var addressList = Utils.String2List(addresses)?.ToList(); + if (addressList is not { Count: > 0 }) + { + return servers; + } + servers.AddRange(addressList.Select(ParseDnsAddress).Where(s => s != null)); + return servers; + } + private static Server4Sbox? ParseDnsAddress(string address) { - var addressFirst = address?.Split(address.Contains(',') ? ',' : ';').FirstOrDefault()?.Trim(); + var addressFirst = Utils.String2List(address)?.FirstOrDefault()?.Trim(); if (string.IsNullOrEmpty(addressFirst)) { return null; @@ -547,10 +672,33 @@ public partial class CoreConfigSingboxService { server.server_port = port; } - if ((server.type == "https" || server.type == "h3") && !string.IsNullOrEmpty(path) && path != "/") + if (server.type is "https" or "h3" && !string.IsNullOrEmpty(path) && path != "/") { server.path = path; } return server; } + + private bool UseDirectDns() + { + var routing = context.RoutingItem; + var useDirectDns = false; + if (routing == null) + { + return useDirectDns; + } + var rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; + + if (rules?.LastOrDefault() is not { OutboundTag: Global.DirectTag } lastRule) + { + return useDirectDns; + } + var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0; + var noProcess = lastRule.Process == null || lastRule.Process.Count == 0; + var isAnyIp = lastRule.Ip == null || lastRule.Ip.Count == 0 || lastRule.Ip.Contains("0.0.0.0/0"); + var isAnyPort = string.IsNullOrEmpty(lastRule.Port) || lastRule.Port == "0-65535"; + var isAnyNetwork = string.IsNullOrEmpty(lastRule.Network) || lastRule.Network == "tcp,udp"; + useDirectDns = noDomain && noProcess && isAnyIp && isAnyPort && isAnyNetwork; + return useDirectDns; + } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index aae1a65c..96117897 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -244,7 +244,7 @@ public partial class CoreConfigSingboxService var resolveRule = new Rule4Sbox { action = "resolve", - strategy = domainStrategy + strategy = domainStrategy, }; if (_config.RoutingBasicItem.DomainStrategy == Global.IPOnDemand) { diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs index d38e9945..4a6b449c 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs @@ -79,6 +79,7 @@ public partial class CoreConfigSingboxService //Add ruleset srs _coreConfig.route.rule_set = []; + var containRemoteRuleset = false; foreach (var item in new HashSet(ruleSets)) { if (item.IsNullOrEmpty()) @@ -94,11 +95,13 @@ public partial class CoreConfigSingboxService type = "local", format = "binary", tag = item, - path = pathSrs + path = pathSrs, }; } else { + containRemoteRuleset = true; + var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl) ? Global.SingboxRulesetUrl : _config.ConstItem.SrsSourceUrl; @@ -109,11 +112,21 @@ public partial class CoreConfigSingboxService format = "binary", tag = item, url = string.Format(srsUrl, item.StartsWith(geosite) ? geosite : geoip, item), - download_detour = Global.ProxyTag + http_client = Global.SingboxSrsDownloadHttpClientTag, }; } } _coreConfig.route.rule_set.Add(customRuleset); } + + if (containRemoteRuleset) + { + _coreConfig.http_clients ??= []; + _coreConfig.http_clients.Add(new() + { + tag = Global.SingboxSrsDownloadHttpClientTag, + detour = Global.ProxyTag, + }); + } } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs index ff8e5df2..bacbdf1d 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -197,8 +197,7 @@ public partial class CoreConfigV2rayService if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs)) { - expectedIPs = simpleDNSItem.DirectExpectedIPs - .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) + expectedIPs = (Utils.String2List(simpleDNSItem.DirectExpectedIPs) ?? []) .Select(s => s.Trim()) .Where(s => !string.IsNullOrEmpty(s)) .ToHashSet(); @@ -335,7 +334,7 @@ public partial class CoreConfigV2rayService static List ParseDnsAddresses(string? dnsInput, string defaultAddress) { - var addresses = dnsInput?.Split(dnsInput.Contains(',') ? ',' : ';') + var addresses = (Utils.String2List(dnsInput) ?? []) .Select(addr => addr.Trim()) .Where(addr => !string.IsNullOrEmpty(addr)) .Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr)