Fix hosts matching (#8890)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run

* Fix hosts matching

* Fix hosts resolve rule

* Fix
This commit is contained in:
DHR60 2026-03-04 12:13:06 +00:00 committed by GitHub
parent 81da72bb39
commit b8f7cc0768
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 123 additions and 33 deletions

View file

@ -93,7 +93,23 @@ public partial class CoreConfigSingboxService
foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts)) 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) foreach (var host in hostsDns.predefined)
@ -179,44 +195,66 @@ public partial class CoreConfigSingboxService
foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts)) foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts))
{ {
var predefined = kvp.Value.First(); var predefined = kvp.Value.First();
if (predefined.IsNullOrEmpty() || Utils.IsIpAddress(predefined)) if (predefined.IsNullOrEmpty())
{ {
continue; continue;
} }
if (predefined.StartsWith('#') && int.TryParse(predefined.AsSpan(1), out var rcode)) var rule = new Rule4Sbox()
{ {
// xray syntactic sugar for predefined query_type = [1, 5, 28], // A, CNAME and AAAA
// 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],
action = "predefined", action = "predefined",
rcode = "NOERROR", 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<string> { $"*. 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<string> { $"*. IN AAAA {predefined}" };
}
else
{
rule.answer = new List<string> { $"*. IN A {predefined}" };
}
}
else
{
continue;
}
_coreConfig.dns.rules.Add(rule);
} }
if (simpleDnsItem.BlockBindingQuery == true) if (simpleDnsItem.BlockBindingQuery == true)

View file

@ -84,11 +84,58 @@ public partial class CoreConfigSingboxService
} }
if (hostsDomains.Count > 0) if (hostsDomains.Count > 0)
{ {
_coreConfig.route.rules.Add(new() var hostsResolveRule = new Rule4Sbox
{ {
action = "resolve", 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() _coreConfig.route.rules.Add(new()
@ -355,6 +402,11 @@ public partial class CoreConfigSingboxService
rule.domain_keyword ??= []; rule.domain_keyword ??= [];
rule.domain_keyword?.Add(domain.Substring(8)); rule.domain_keyword?.Add(domain.Substring(8));
} }
else if (domain.StartsWith("dotless:"))
{
rule.domain_keyword ??= [];
rule.domain_keyword?.Add(domain.Substring(8));
}
else else
{ {
rule.domain_keyword ??= []; rule.domain_keyword ??= [];