diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index 7a5074bb..afab27d9 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -93,7 +93,23 @@ public partial class CoreConfigSingboxService foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts)) { - hostsDns.predefined[kvp.Key] = kvp.Value.Where(Utils.IsIpAddress).ToList(); + // only allow full match + // like example.com and full:example.com, + // but not domain:example.com, keyword:example.com or regex:example.com etc. + var testRule = new Rule4Sbox(); + if (!ParseV2Domain(kvp.Key, testRule)) + { + continue; + } + if (testRule.domain_keyword?.Count > 0 && !kvp.Key.Contains(':')) + { + testRule.domain = testRule.domain_keyword; + testRule.domain_keyword = null; + } + if (testRule.domain?.Count == 1) + { + hostsDns.predefined[testRule.domain.First()] = kvp.Value.Where(Utils.IsIpAddress).ToList(); + } } foreach (var host in hostsDns.predefined) @@ -179,44 +195,66 @@ public partial class CoreConfigSingboxService foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts)) { var predefined = kvp.Value.First(); - if (predefined.IsNullOrEmpty() || Utils.IsIpAddress(predefined)) + if (predefined.IsNullOrEmpty()) { continue; } - if (predefined.StartsWith('#') && int.TryParse(predefined.AsSpan(1), out var rcode)) + var rule = new Rule4Sbox() { - // xray syntactic sugar for predefined - // etc. #0 -> NOERROR - _coreConfig.dns.rules.Add(new() - { - query_type = [1, 28], - domain = [kvp.Key], - action = "predefined", - rcode = rcode switch - { - 0 => "NOERROR", - 1 => "FORMERR", - 2 => "SERVFAIL", - 3 => "NXDOMAIN", - 4 => "NOTIMP", - 5 => "REFUSED", - _ => "NOERROR", - }, - }); - continue; - } - // CNAME record - Rule4Sbox rule = new() - { - query_type = [1, 28], + query_type = [1, 5, 28], // A, CNAME and AAAA action = "predefined", rcode = "NOERROR", - answer = [$"*. IN CNAME {predefined}."], }; - if (ParseV2Domain(kvp.Key, rule)) + if (!ParseV2Domain(kvp.Key, rule)) { - _coreConfig.dns.rules.Add(rule); + continue; } + // see: https://xtls.github.io/en/config/dns.html#dnsobject + // The matching format (domain:, full:, etc.) is the same as the domain + // in the commonly used Routing System. The difference is that without a prefix, + // it defaults to using the full: prefix (similar to the common hosts file syntax). + if (rule.domain_keyword?.Count > 0 && !kvp.Key.Contains(':')) + { + rule.domain = rule.domain_keyword; + rule.domain_keyword = null; + } + // example.com #0 -> example.com with NOERROR + if (predefined.StartsWith('#') && int.TryParse(predefined.AsSpan(1), out var rcode)) + { + rule.rcode = rcode switch + { + 0 => "NOERROR", + 1 => "FORMERR", + 2 => "SERVFAIL", + 3 => "NXDOMAIN", + 4 => "NOTIMP", + 5 => "REFUSED", + _ => "NOERROR", + }; + } + else if (Utils.IsDomain(predefined)) + { + // example.com CNAME target.com -> example.com with CNAME target.com + rule.answer = new List { $"*. IN CNAME {predefined}." }; + } + else if (Utils.IsIpAddress(predefined) && (rule.domain?.Count ?? 0) == 0) + { + // not full match, but an IP address, treat it as predefined answer + if (Utils.IsIpv6(predefined)) + { + rule.answer = new List { $"*. IN AAAA {predefined}" }; + + } + else + { + rule.answer = new List { $"*. IN A {predefined}" }; + } + } + else + { + continue; + } + _coreConfig.dns.rules.Add(rule); } if (simpleDnsItem.BlockBindingQuery == true) diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 9bcb9adf..45620c41 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -84,11 +84,58 @@ public partial class CoreConfigSingboxService } if (hostsDomains.Count > 0) { - _coreConfig.route.rules.Add(new() + var hostsResolveRule = new Rule4Sbox { action = "resolve", - domain = hostsDomains, - }); + }; + var hostsCounter = 0; + foreach (var host in hostsDomains) + { + var domainRule = new Rule4Sbox(); + if (!ParseV2Domain(host, domainRule)) + { + continue; + } + if (domainRule.domain_keyword?.Count > 0 && !host.Contains(':')) + { + domainRule.domain = domainRule.domain_keyword; + domainRule.domain_keyword = null; + } + if (domainRule.domain?.Count > 0) + { + hostsResolveRule.domain ??= []; + hostsResolveRule.domain.AddRange(domainRule.domain); + hostsCounter++; + } + else if (domainRule.domain_keyword?.Count > 0) + { + hostsResolveRule.domain_keyword ??= []; + hostsResolveRule.domain_keyword.AddRange(domainRule.domain_keyword); + hostsCounter++; + } + else if (domainRule.domain_suffix?.Count > 0) + { + hostsResolveRule.domain_suffix ??= []; + hostsResolveRule.domain_suffix.AddRange(domainRule.domain_suffix); + hostsCounter++; + } + else if (domainRule.domain_regex?.Count > 0) + { + hostsResolveRule.domain_regex ??= []; + hostsResolveRule.domain_regex.AddRange(domainRule.domain_regex); + hostsCounter++; + } + else if (domainRule.geosite?.Count > 0) + { + hostsResolveRule.geosite ??= []; + hostsResolveRule.geosite.AddRange(domainRule.geosite); + hostsCounter++; + } + } + if (hostsCounter > 0) + { + _coreConfig.route.rules.Add(hostsResolveRule); + } } _coreConfig.route.rules.Add(new() @@ -355,6 +402,11 @@ public partial class CoreConfigSingboxService rule.domain_keyword ??= []; rule.domain_keyword?.Add(domain.Substring(8)); } + else if (domain.StartsWith("dotless:")) + { + rule.domain_keyword ??= []; + rule.domain_keyword?.Add(domain.Substring(8)); + } else { rule.domain_keyword ??= [];