diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 91b4b6d7..e5a6ab29 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // 此代码由工具生成。 // 运行时版本:4.0.30319.42000 @@ -3832,7 +3832,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Validate Direct Expected IPs 的本地化字符串。 + /// 查找类似 Validate Regional Domain IPs 的本地化字符串。 /// public static string TbValidateDirectExpectedIPs { get { @@ -3841,7 +3841,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 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 的本地化字符串。 /// public static string TbValidateDirectExpectedIPsDesc { get { diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 30cea669..bbdc46ad 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1456,9 +1456,9 @@ Advanced DNS Settings - Validate Direct Expected IPs + Validate Regional Domain IPs - 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 \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index d9ef04bb..31d216ff 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1456,9 +1456,9 @@ Advanced DNS Settings - Validate Direct Expected IPs + Validate Regional Domain IPs - 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 \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 21ab6be6..6e00715d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1456,9 +1456,9 @@ Advanced DNS Settings - Validate Direct Expected IPs + Validate Regional Domain IPs - 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 \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index d45cfb2c..da84bea7 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1456,9 +1456,9 @@ Advanced DNS Settings - Validate Direct Expected IPs + Validate Regional Domain IPs - 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 \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index ced3d7ec..a544a0b0 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1453,9 +1453,9 @@ DNS 进阶设置 - 校验直连期望 IP + 校验相应地区域名 IP - 配置后,会对返回的 IP 的进行校验,只返回期望 IP + 配置后,会对相应地区域名(如 geosite:cn)的返回 IP 进行校验,仅返回期望 IP \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index d8a2dbc6..27a7b308 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1453,9 +1453,9 @@ Advanced DNS Settings - Validate Direct Expected IPs + Validate Regional Domain IPs - 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 \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs index 6011b28d..684cc3bc 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs @@ -1704,92 +1704,124 @@ public class CoreConfigSingboxService { singboxConfig.dns ??= new Dns4Sbox(); singboxConfig.dns.rules ??= new List(); - // 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 { 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 { 64, 65 }, + action = "predefined", + rcode = "NOTIMP" + } }); var routing = await ConfigHandler.GetDefaultRouting(_config); - if (routing != null) - { - var rules = JsonUtils.Deserialize>(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>(routing.RuleSet) ?? []; + var expectedIPCidr = new List(); + var expectedIPsRegions = new List(); + var regionNames = new HashSet(); + + 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 { "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(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 { "A" }; + } + + singboxConfig.dns.rules.Add(rule); + } + + return 0; } private static Server4Sbox? ParseDnsAddress(string address) diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs index e5589637..c09b32b6 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs @@ -1167,194 +1167,155 @@ public class CoreConfigV2rayService private async Task 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 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 { defaultAddress }; + return addresses.Count > 0 ? addresses : new List { 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 domains, List? 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(); var directGeositeList = new List(); var proxyDomainList = new List(); var proxyGeositeList = new List(); + var expectedDomainList = new List(); + var expectedIPs = new List(); + var regionNames = new HashSet(); + + 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>(routing.RuleSet); - foreach (var item in rules ?? []) + var rules = JsonUtils.Deserialize>(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(); - var options = new JsonSerializerOptions + void AddDnsServers(List dnsAddresses, List domains, List? 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 GenDnsHosts(V2rayConfig v2rayConfig, DNSItem dNSItem) diff --git a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs index 0bb2bbbc..5a8c7f6f 100644 --- a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs @@ -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); });