Optimize ExpectedIPs Logic

DHR60 2025-07-22 18:09:15 +08:00
parent 5331e2eeff
commit 4105031446
10 changed files with 220 additions and 227 deletions

View File

@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
@ -3832,7 +3832,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Validate Direct Expected IPs 的本地化字符串。
/// 查找类似 Validate Regional Domain IPs 的本地化字符串。
/// </summary>
public static string TbValidateDirectExpectedIPs {
get {
@ -3841,7 +3841,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 After configuration, validates returned IPs, returning only expected IPs 的本地化字符串。
/// 查找类似 When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs 的本地化字符串。
/// </summary>
public static string TbValidateDirectExpectedIPsDesc {
get {

View File

@ -1456,9 +1456,9 @@
<value>Advanced DNS Settings</value>
</data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Direct Expected IPs</value>
<value>Validate Regional Domain IPs</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>After configuration, validates returned IPs, returning only expected IPs</value>
<value>When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs</value>
</data>
</root>

View File

@ -1456,9 +1456,9 @@
<value>Advanced DNS Settings</value>
</data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Direct Expected IPs</value>
<value>Validate Regional Domain IPs</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>After configuration, validates returned IPs, returning only expected IPs</value>
<value>When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs</value>
</data>
</root>

View File

@ -1456,9 +1456,9 @@
<value>Advanced DNS Settings</value>
</data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Direct Expected IPs</value>
<value>Validate Regional Domain IPs</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>After configuration, validates returned IPs, returning only expected IPs</value>
<value>When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs</value>
</data>
</root>

View File

@ -1456,9 +1456,9 @@
<value>Advanced DNS Settings</value>
</data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Direct Expected IPs</value>
<value>Validate Regional Domain IPs</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>After configuration, validates returned IPs, returning only expected IPs</value>
<value>When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs</value>
</data>
</root>

View File

@ -1453,9 +1453,9 @@
<value>DNS 进阶设置</value>
</data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>校验直连期望 IP</value>
<value>校验相应地区域名 IP</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>配置后,会对返回的 IP 的进行校验,只返回期望 IP</value>
<value>配置后,会对相应地区域名(如 geosite:cn的返回 IP 进行校验,仅返回期望 IP</value>
</data>
</root>

View File

@ -1453,9 +1453,9 @@
<value>Advanced DNS Settings</value>
</data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Direct Expected IPs</value>
<value>Validate Regional Domain IPs</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>After configuration, validates returned IPs, returning only expected IPs</value>
<value>When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs</value>
</data>
</root>

View File

@ -1704,92 +1704,124 @@ public class CoreConfigSingboxService
{
singboxConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
// hosts
singboxConfig.dns.rules.Add(new Rule4Sbox
{
ip_accept_any = true,
server = "dns_hosts",
});
// clash mode
singboxConfig.dns.rules.Add(new Rule4Sbox
{
server = "dns_remote",
strategy = dNSItem.SingboxStrategy4Proxy.IsNullOrEmpty() ? null : dNSItem.SingboxStrategy4Proxy,
clash_mode = ERuleMode.Global.ToString()
});
singboxConfig.dns.rules.Add(new Rule4Sbox
{
server = "dns_direct",
strategy = dNSItem.SingboxStrategy4Direct.IsNullOrEmpty() ? null : dNSItem.SingboxStrategy4Direct,
clash_mode = ERuleMode.Direct.ToString()
});
// block binding query
singboxConfig.dns.rules.Add(new Rule4Sbox
{
query_type = new List<int> { 64, 65 },
action = "predefined",
rcode = "NOTIMP"
singboxConfig.dns.rules.AddRange(new[]
{
new Rule4Sbox { ip_accept_any = true, server = "dns_hosts" },
new Rule4Sbox
{
server = "dns_remote",
strategy = string.IsNullOrEmpty(dNSItem.SingboxStrategy4Proxy) ? null : dNSItem.SingboxStrategy4Proxy,
clash_mode = ERuleMode.Global.ToString()
},
new Rule4Sbox
{
server = "dns_direct",
strategy = string.IsNullOrEmpty(dNSItem.SingboxStrategy4Direct) ? null : dNSItem.SingboxStrategy4Direct,
clash_mode = ERuleMode.Direct.ToString()
},
new Rule4Sbox
{
query_type = new List<int> { 64, 65 },
action = "predefined",
rcode = "NOTIMP"
}
});
var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing != null)
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var item in rules ?? [])
{
if (!item.Enabled)
{
continue;
}
if (item.Domain == null || item.Domain.Count == 0)
{
continue;
}
var rule = new Rule4Sbox();
if (routing == null)
return 0;
var countDomain = 0;
foreach (var it in item.Domain)
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
var expectedIPCidr = new List<string>();
var expectedIPsRegions = new List<string>();
var regionNames = new HashSet<string>();
if (!string.IsNullOrEmpty(dNSItem?.DirectExpectedIPs))
{
var ipItems = dNSItem.DirectExpectedIPs
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToList();
foreach (var ip in ipItems)
{
if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase))
{
if (ParseV2Domain(it, rule))
var region = ip["geoip:".Length..];
if (!string.IsNullOrEmpty(region))
{
countDomain++;
expectedIPsRegions.Add(region);
regionNames.Add(region);
regionNames.Add($"geolocation-{region}");
regionNames.Add($"tld-{region}");
}
}
if (countDomain <= 0)
else
{
continue;
expectedIPCidr.Add(ip);
}
if (item.OutboundTag == Global.DirectTag)
{
rule.server = "dns_direct";
rule.strategy = dNSItem.SingboxStrategy4Direct.IsNullOrEmpty() ? null : dNSItem.SingboxStrategy4Direct;
if (!dNSItem.DirectExpectedIPs.IsNullOrEmpty())
{
rule.rule_set = new() { dNSItem.DirectExpectedIPs };
}
}
else if (item.OutboundTag == Global.ProxyTag)
{
if (dNSItem.FakeIP == true)
{
var rule4Fake = JsonUtils.DeepCopy(rule);
rule4Fake.server = "dns-fake";
singboxConfig.dns.rules.Add(rule4Fake);
}
rule.server = "dns_remote";
rule.strategy = dNSItem.SingboxStrategy4Proxy.IsNullOrEmpty() ? null : dNSItem.SingboxStrategy4Proxy;
}
else if (item.OutboundTag == Global.BlockTag)
{
rule.action = "predefined";
rule.rcode = "NOERROR";
rule.answer = new List<string> { "A" };
}
singboxConfig.dns.rules.Add(rule);
}
}
return await Task.FromResult(0);
foreach (var item in rules)
{
if (!item.Enabled || item.Domain is null || item.Domain.Count == 0)
{
continue;
}
var rule = new Rule4Sbox();
var validDomains = item.Domain.Count(it => ParseV2Domain(it, rule));
if (validDomains <= 0)
{
continue;
}
if (item.OutboundTag == Global.DirectTag)
{
rule.server = "dns_direct";
rule.strategy = string.IsNullOrEmpty(dNSItem.SingboxStrategy4Direct) ? null : dNSItem.SingboxStrategy4Direct;
if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0)
{
var geositeSet = new HashSet<string>(rule.geosite);
if (regionNames.Intersect(geositeSet).Any())
{
if (expectedIPsRegions.Count > 0)
{
rule.geoip = expectedIPsRegions;
}
if (expectedIPCidr.Count > 0)
{
rule.ip_cidr = expectedIPCidr;
}
}
}
}
else if (item.OutboundTag == Global.ProxyTag)
{
if (dNSItem.FakeIP == true)
{
var rule4Fake = JsonUtils.DeepCopy(rule);
rule4Fake.server = "dns-fake";
singboxConfig.dns.rules.Add(rule4Fake);
}
rule.server = "dns_remote";
rule.strategy = string.IsNullOrEmpty(dNSItem.SingboxStrategy4Proxy) ? null : dNSItem.SingboxStrategy4Proxy;
}
else if (item.OutboundTag == Global.BlockTag)
{
rule.action = "predefined";
rule.rcode = "NOERROR";
rule.answer = new List<string> { "A" };
}
singboxConfig.dns.rules.Add(rule);
}
return 0;
}
private static Server4Sbox? ParseDnsAddress(string address)

View File

@ -1167,194 +1167,155 @@ public class CoreConfigV2rayService
private async Task<int> GenDnsServers(ProfileItem? node, V2rayConfig v2rayConfig, DNSItem dNSItem)
{
var directDNSAddress = dNSItem?.DirectDNS?
.Split(dNSItem.DirectDNS?.Contains(',') == true ? ',' : ';')
.Select(addr => addr.Trim())
.Where(addr => !string.IsNullOrEmpty(addr))
.Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr)
.Distinct()
.ToList();
if (directDNSAddress != null && directDNSAddress.Count == 0)
static List<string> ParseDnsAddresses(string? dnsInput, string defaultAddress)
{
directDNSAddress = new() { Global.DomainDirectDNSAddress.FirstOrDefault() };
var addresses = dnsInput?.Split(dnsInput.Contains(',') ? ',' : ';')
.Select(addr => addr.Trim())
.Where(addr => !string.IsNullOrEmpty(addr))
.Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr)
.Distinct()
.ToList() ?? new List<string> { defaultAddress };
return addresses.Count > 0 ? addresses : new List<string> { defaultAddress };
}
var remoteDNSAddress = dNSItem?.RemoteDNS?
.Split(dNSItem.RemoteDNS?.Contains(',') == true ? ',' : ';')
.Select(addr => addr.Trim())
.Where(addr => !string.IsNullOrEmpty(addr))
.Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr)
.Distinct()
.ToList();
if (remoteDNSAddress != null && remoteDNSAddress.Count == 0)
static object CreateDnsServer(string dnsAddress, List<string> domains, List<string>? expectedIPs = null)
{
remoteDNSAddress = new() { Global.DomainRemoteDNSAddress.FirstOrDefault() };
var dnsServer = new DnsServer4Ray
{
address = dnsAddress,
skipFallback = true,
domains = domains.Count > 0 ? domains : null,
expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null
};
return JsonUtils.SerializeToNode(dnsServer, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
}
var directDNSAddress = ParseDnsAddresses(dNSItem?.DirectDNS, Global.DomainDirectDNSAddress.FirstOrDefault());
var remoteDNSAddress = ParseDnsAddresses(dNSItem?.RemoteDNS, Global.DomainRemoteDNSAddress.FirstOrDefault());
var directDomainList = new List<string>();
var directGeositeList = new List<string>();
var proxyDomainList = new List<string>();
var proxyGeositeList = new List<string>();
var expectedDomainList = new List<string>();
var expectedIPs = new List<string>();
var regionNames = new HashSet<string>();
if (!string.IsNullOrEmpty(dNSItem?.DirectExpectedIPs))
{
expectedIPs = dNSItem.DirectExpectedIPs
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToList();
foreach (var ip in expectedIPs)
{
if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase))
{
var region = ip["geoip:".Length..];
if (!string.IsNullOrEmpty(region))
{
regionNames.Add($"geosite:{region}");
regionNames.Add($"geosite:geolocation-{region}");
regionNames.Add($"geosite:tld-{region}");
}
}
}
}
var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing != null)
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var item in rules ?? [])
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
foreach (var item in rules)
{
if (!item.Enabled)
if (!item.Enabled || item.Domain is null || item.Domain.Count == 0)
{
continue;
}
if (item.Domain == null || item.Domain.Count == 0)
foreach (var domain in item.Domain)
{
continue;
}
if (item.OutboundTag == Global.DirectTag)
{
foreach (var domain in item.Domain)
if (domain.StartsWith('#'))
continue;
var normalizedDomain = domain.Replace(Global.RoutingRuleComma, ",");
if (item.OutboundTag == Global.DirectTag)
{
if (domain.StartsWith('#'))
if (normalizedDomain.StartsWith("geosite:"))
{
continue;
}
var domain1 = domain.Replace(Global.RoutingRuleComma, ",");
if (domain1.StartsWith("geosite:"))
{
directGeositeList.Add(domain1);
(regionNames.Contains(normalizedDomain) ? expectedDomainList : directGeositeList).Add(normalizedDomain);
}
else
{
directDomainList.Add(domain1);
directDomainList.Add(normalizedDomain);
}
}
}
else if (item.OutboundTag == Global.ProxyTag)
{
foreach (var domain in item.Domain)
else if (item.OutboundTag == Global.ProxyTag)
{
if (domain.StartsWith('#'))
if (normalizedDomain.StartsWith("geosite:"))
{
continue;
}
var domain1 = domain.Replace(Global.RoutingRuleComma, ",");
if (domain1.StartsWith("geosite:"))
{
proxyGeositeList.Add(domain1);
proxyGeositeList.Add(normalizedDomain);
}
else
{
proxyDomainList.Add(domain1);
proxyDomainList.Add(normalizedDomain);
}
}
}
}
}
if (Utils.IsDomain(node.Address))
if (Utils.IsDomain(node?.Address))
{
directDomainList.Add(node.Address);
}
var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
if (subItem is not null)
{
// Previous proxy
var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode is not null
&& prevNode.ConfigType != EConfigType.Custom
&& prevNode.ConfigType != EConfigType.Hysteria2
&& prevNode.ConfigType != EConfigType.TUIC
&& prevNode.ConfigType != EConfigType.Anytls
&& Utils.IsDomain(prevNode.Address))
{
directDomainList.Add(prevNode.Address);
}
// Next proxy
var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode is not null
&& nextNode.ConfigType != EConfigType.Custom
&& nextNode.ConfigType != EConfigType.Hysteria2
&& nextNode.ConfigType != EConfigType.TUIC
&& nextNode.ConfigType != EConfigType.Anytls
&& Utils.IsDomain(nextNode.Address))
if (node?.Subid is not null)
{
var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
if (subItem is not null)
{
directDomainList.Add(nextNode.Address);
foreach (var profile in new[] { subItem.PrevProfile, subItem.NextProfile })
{
var profileNode = await AppHandler.Instance.GetProfileItemViaRemarks(profile);
if (profileNode is not null &&
profileNode.ConfigType is not (EConfigType.Custom or EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) &&
Utils.IsDomain(profileNode.Address))
{
directDomainList.Add(profileNode.Address);
}
}
}
}
v2rayConfig.dns ??= new Dns4Ray();
v2rayConfig.dns.servers ??= new List<object>();
var options = new JsonSerializerOptions
void AddDnsServers(List<string> dnsAddresses, List<string> domains, List<string>? expectedIPs = null)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
if (proxyDomainList.Count > 0)
{
foreach (var dnsDomain in remoteDNSAddress)
if (domains.Count > 0)
{
var dnsServer = new DnsServer4Ray
foreach (var dnsAddress in dnsAddresses)
{
address = dnsDomain,
skipFallback = true,
domains = proxyDomainList
};
v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer, options));
v2rayConfig.dns.servers.Add(CreateDnsServer(dnsAddress, domains, expectedIPs));
}
}
}
if (directDomainList.Count > 0)
{
foreach (var dnsDomain in directDNSAddress)
{
var dnsServer = new DnsServer4Ray
{
address = dnsDomain,
skipFallback = true,
domains = directDomainList,
expectedIPs = dNSItem.DirectExpectedIPs.IsNullOrEmpty() ? null : new() { dNSItem.DirectExpectedIPs }
};
v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer, options));
}
}
if (proxyGeositeList.Count > 0)
{
foreach (var dnsDomain in remoteDNSAddress)
{
var dnsServer = new DnsServer4Ray()
{
address = dnsDomain,
skipFallback = true,
domains = proxyGeositeList
};
v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer, options));
}
}
if (directGeositeList.Count > 0)
{
foreach (var dnsDomain in directDNSAddress)
{
var dnsServer = new DnsServer4Ray()
{
address = dnsDomain,
skipFallback = true,
domains = directGeositeList,
expectedIPs = dNSItem.DirectExpectedIPs.IsNullOrEmpty() ? null : new() { dNSItem.DirectExpectedIPs }
};
v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer, options));
}
}
AddDnsServers(remoteDNSAddress, proxyDomainList);
AddDnsServers(directDNSAddress, directDomainList);
AddDnsServers(remoteDNSAddress, proxyGeositeList);
AddDnsServers(directDNSAddress, directGeositeList);
AddDnsServers(directDNSAddress, expectedDomainList, expectedIPs);
// fallback DNS server
// TODO: Select fallback DNS server based on routing rules
foreach (var dnsDomain in remoteDNSAddress)
{
v2rayConfig.dns.servers.Add(dnsDomain);
}
return await Task.FromResult(0);
v2rayConfig.dns.servers.AddRange(remoteDNSAddress);
return 0;
}
private async Task<int> GenDnsHosts(V2rayConfig v2rayConfig, DNSItem dNSItem)

View File

@ -32,15 +32,15 @@ public partial class DNSSettingWindow
this.Bind(ViewModel, vm => vm.AddCommonHosts, v => v.togAddCommonHosts.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FakeIP, v => v.togFakeIP.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxOutboundsResolveDNS, v => v.cmbSBResolverDNS.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxFinalResolveDNS, v => v.cmbSBFinalResolverDNS.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxOutboundsResolveDNS, v => v.cmbSBResolverDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxFinalResolveDNS, v => v.cmbSBFinalResolverDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RayStrategy4Freedom, v => v.cmbRayFreedomDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hosts, v => v.txtHosts.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});