From 07e173eab1b01ddf9f1f4c543151d44664b911e6 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Tue, 21 Oct 2025 17:28:48 +0800 Subject: [PATCH] Bootstrap DNS (#8160) Also fix the handling of IPv6 domains --- v2rayN/ServiceLib/Common/Utils.cs | 104 ++++++++++++++++++ v2rayN/ServiceLib/Handler/ConfigHandler.cs | 7 +- v2rayN/ServiceLib/Models/ConfigItems.cs | 1 + v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 18 +++ v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 6 + v2rayN/ServiceLib/Resx/ResUI.hu.resx | 6 + v2rayN/ServiceLib/Resx/ResUI.resx | 6 + v2rayN/ServiceLib/Resx/ResUI.ru.resx | 6 + v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 6 + v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 6 + .../CoreConfig/Singbox/SingboxDnsService.cs | 81 +++----------- .../CoreConfig/V2ray/V2rayDnsService.cs | 33 ++++++ .../ViewModels/DNSSettingViewModel.cs | 3 + .../Views/DNSSettingWindow.axaml | 37 +++++-- .../Views/DNSSettingWindow.axaml.cs | 2 + v2rayN/v2rayN/Views/DNSSettingWindow.xaml | 38 +++++-- v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs | 2 + 17 files changed, 280 insertions(+), 82 deletions(-) diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index 8adf9d83..5ca96a88 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -355,6 +355,110 @@ public class Utils return userHostsMap; } + /// + /// Parse a possibly non-standard URL into scheme, domain, port, and path. + /// If parsing fails, the entire input is returned as domain, and others are empty or zero. + /// + /// Input URL or string + /// (domain, scheme, port, path) + public static (string domain, string scheme, int port, string path) ParseUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return ("", "", 0, ""); + } + + // 1. First, try to parse using the standard Uri class. + if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && !string.IsNullOrEmpty(uri.Host)) + { + var scheme = uri.Scheme; + var domain = uri.Host; + var port = uri.IsDefaultPort ? 0 : uri.Port; + var path = uri.PathAndQuery; + return (domain, scheme, port, path); + } + + // 2. Try to handle more general cases with a regular expression, including non-standard schemes. + // This regex captures the scheme (optional), authority (host+port), and path (optional). + var match = Regex.Match(url, @"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):/{2,})?([^/?#]+)([^?#]*)?.*$"); + + if (match.Success) + { + var scheme = match.Groups[1].Value; + var authority = match.Groups[2].Value; + var path = match.Groups[3].Value; + + // Remove userinfo from the authority part. + var atIndex = authority.LastIndexOf('@'); + if (atIndex > 0) + { + authority = authority.Substring(atIndex + 1); + } + + var (domain, port) = ParseAuthority(authority); + + // If the parsed domain is empty, it means the authority part is malformed, so trigger the fallback. + if (!string.IsNullOrEmpty(domain)) + { + return (domain, scheme, port, path); + } + } + + // 3. If all of the above fails, execute the final fallback strategy. + return (url, "", 0, ""); + } + + /// + /// Helper function to parse domain and port from the authority part, with correct handling for IPv6. + /// + private static (string domain, int port) ParseAuthority(string authority) + { + if (string.IsNullOrEmpty(authority)) + { + return ("", 0); + } + + var port = 0; + var domain = authority; + + // Handle IPv6 addresses, e.g., "[2001:db8::1]:443" + if (authority.StartsWith("[") && authority.Contains("]")) + { + int closingBracketIndex = authority.LastIndexOf(']'); + if (closingBracketIndex < authority.Length - 1 && authority[closingBracketIndex + 1] == ':') + { + // Port exists + var portStr = authority.Substring(closingBracketIndex + 2); + if (int.TryParse(portStr, out var portNum)) + { + port = portNum; + } + domain = authority.Substring(0, closingBracketIndex + 1); + } + else + { + // No port + domain = authority; + } + } + else // Handle IPv4 or domain names + { + var lastColonIndex = authority.LastIndexOf(':'); + // Ensure there are digits after the colon and that this colon is not part of an IPv6 address. + if (lastColonIndex > 0 && lastColonIndex < authority.Length - 1 && authority.Substring(lastColonIndex + 1).All(char.IsDigit)) + { + var portStr = authority.Substring(lastColonIndex + 1); + if (int.TryParse(portStr, out var portNum)) + { + port = portNum; + domain = authority.Substring(0, lastColonIndex); + } + } + } + + return (domain, port); + } + #endregion 转换函数 #region 数据检查 diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index fd93d8be..2840fab7 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -112,10 +112,8 @@ public static class ConfigHandler config.ConstItem ??= new ConstItem(); config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); - if (config.SimpleDNSItem.GlobalFakeIp is null) - { - config.SimpleDNSItem.GlobalFakeIp = true; - } + config.SimpleDNSItem.GlobalFakeIp ??= true; + config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault(); config.SpeedTestItem ??= new(); if (config.SpeedTestItem.SpeedTestTimeout < 10) @@ -2273,6 +2271,7 @@ public static class ConfigHandler BlockBindingQuery = true, DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(), RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(), + BootstrapDNS = Global.DomainPureIPDNSAddress.FirstOrDefault(), }; } diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs index 985c3a88..531a99f4 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -263,6 +263,7 @@ public class SimpleDNSItem public bool? BlockBindingQuery { get; set; } public string? DirectDNS { get; set; } public string? RemoteDNS { get; set; } + public string? BootstrapDNS { get; set; } public string? RayStrategy4Freedom { get; set; } public string? SingboxStrategy4Direct { get; set; } public string? SingboxStrategy4Proxy { get; set; } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 5bf7eb6e..3b12a5ee 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -2517,6 +2517,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Bootstrap DNS 的本地化字符串。 + /// + public static string TbBootstrapDNS { + get { + return ResourceManager.GetString("TbBootstrapDNS", resourceCulture); + } + } + + /// + /// 查找类似 Resolve DNS server domains, requires IP 的本地化字符串。 + /// + public static string TbBootstrapDNSTips { + get { + return ResourceManager.GetString("TbBootstrapDNSTips", resourceCulture); + } + } + /// /// 查找类似 Browse 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 7bcf40f5..02235c2d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1590,4 +1590,10 @@ You can set separate rules for Routing and DNS, or select "ALL" to apply to both + + Bootstrap DNS + + + Resolve DNS server domains, requires IP + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 3ffa2e12..60b56d21 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1590,4 +1590,10 @@ Rule Type + + Bootstrap DNS + + + Resolve DNS server domains, requires IP + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 5447e569..d92df24d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1590,4 +1590,10 @@ Rule Type + + Bootstrap DNS + + + Resolve DNS server domains, requires IP + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 72a63936..7b2e12cb 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1590,4 +1590,10 @@ Rule Type + + Bootstrap DNS + + + Resolve DNS server domains, requires IP + \ 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 a6e297c3..b4d912d2 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1587,4 +1587,10 @@ 可对 Routing 和 DNS 单独设定规则,ALL 则都生效 + + Bootstrap DNS + + + 解析 DNS 服务器域名,需指定为 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 fe10f622..9605afc8 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1587,4 +1587,10 @@ 可对 Routing 和 DNS 单独设定规则,ALL 则都生效 + + Bootstrap DNS + + + Resolve DNS server domains, requires IP + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index b1698ab4..d44c626a 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -138,12 +138,7 @@ public partial class CoreConfigSingboxService private async Task GenDnsDomains(SingboxConfig singboxConfig, SimpleDNSItem? simpleDNSItem) { - var finalDnsAddress = "local"; - if (_config.TunModeItem.EnableTun) - { - finalDnsAddress = "dhcp://auto"; - } - var finalDns = ParseDnsAddress(finalDnsAddress); + var finalDns = ParseDnsAddress(simpleDNSItem.BootstrapDNS); finalDns.tag = Global.SingboxLocalDNSTag; singboxConfig.dns ??= new Dns4Sbox(); singboxConfig.dns.servers ??= new List(); @@ -459,15 +454,19 @@ public partial class CoreConfigSingboxService return server; } - if (addressFirst.StartsWith("dhcp://", StringComparison.OrdinalIgnoreCase)) + var (domain, scheme, port, path) = Utils.ParseUrl(addressFirst); + + if (scheme.Equals("dhcp", StringComparison.OrdinalIgnoreCase)) { - var interface_name = addressFirst.Substring(7); server.type = "dhcp"; - server.Interface = interface_name == "auto" ? null : interface_name; + if ((!domain.IsNullOrEmpty()) && domain != "auto") + { + server.server = domain; + } return server; } - if (!addressFirst.Contains("://")) + if (scheme.IsNullOrEmpty()) { // udp dns server.type = "udp"; @@ -475,63 +474,19 @@ public partial class CoreConfigSingboxService return server; } - try + //server.type = scheme.ToLower(); + // remove "+local" suffix + // TODO: "+local" suffix decide server.detour = "direct" ? + server.type = scheme.Replace("+local", "", StringComparison.OrdinalIgnoreCase).ToLower(); + server.server = domain; + if (port != 0) { - var protocolEndIndex = addressFirst.IndexOf("://", StringComparison.Ordinal); - server.type = addressFirst.Substring(0, protocolEndIndex).ToLower(); - - var uri = new Uri(addressFirst); - server.server = uri.Host; - - if (!uri.IsDefaultPort) - { - server.server_port = uri.Port; - } - - if ((server.type == "https" || server.type == "h3") && !string.IsNullOrEmpty(uri.AbsolutePath) && uri.AbsolutePath != "/") - { - server.path = uri.AbsolutePath; - } + server.server_port = port; } - catch (UriFormatException) + if ((server.type == "https" || server.type == "h3") && !string.IsNullOrEmpty(path) && path != "/") { - var protocolEndIndex = addressFirst.IndexOf("://", StringComparison.Ordinal); - if (protocolEndIndex > 0) - { - server.type = addressFirst.Substring(0, protocolEndIndex).ToLower(); - var remaining = addressFirst.Substring(protocolEndIndex + 3); - - var portIndex = remaining.IndexOf(':'); - var pathIndex = remaining.IndexOf('/'); - - if (portIndex > 0) - { - server.server = remaining.Substring(0, portIndex); - var portPart = pathIndex > portIndex - ? remaining.Substring(portIndex + 1, pathIndex - portIndex - 1) - : remaining.Substring(portIndex + 1); - - if (int.TryParse(portPart, out var parsedPort)) - { - server.server_port = parsedPort; - } - } - else if (pathIndex > 0) - { - server.server = remaining.Substring(0, pathIndex); - } - else - { - server.server = remaining; - } - - if (pathIndex > 0 && (server.type == "https" || server.type == "h3")) - { - server.path = remaining.Substring(pathIndex); - } - } + server.path = path; } - return server; } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs index cb731612..45f0c28a 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -103,6 +103,35 @@ public partial class CoreConfigV2rayService var expectedIPs = new List(); var regionNames = new HashSet(); + var bootstrapDNSAddress = ParseDnsAddresses(simpleDNSItem?.BootstrapDNS, Global.DomainPureIPDNSAddress.FirstOrDefault()); + var dnsServerDomains = new List(); + + foreach (var dns in directDNSAddress) + { + var (domain, _, _, _) = Utils.ParseUrl(dns); + if (domain == "localhost") + { + continue; + } + if (Utils.IsDomain(domain)) + { + dnsServerDomains.Add($"full:{domain}"); + } + } + foreach (var dns in remoteDNSAddress) + { + var (domain, _, _, _) = Utils.ParseUrl(dns); + if (domain == "localhost") + { + continue; + } + if (Utils.IsDomain(domain)) + { + dnsServerDomains.Add($"full:{domain}"); + } + } + dnsServerDomains = dnsServerDomains.Distinct().ToList(); + if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs)) { expectedIPs = simpleDNSItem.DirectExpectedIPs @@ -217,6 +246,10 @@ public partial class CoreConfigV2rayService AddDnsServers(remoteDNSAddress, proxyGeositeList); AddDnsServers(directDNSAddress, directGeositeList); AddDnsServers(directDNSAddress, expectedDomainList, expectedIPs); + if (dnsServerDomains.Count > 0) + { + AddDnsServers(bootstrapDNSAddress, dnsServerDomains); + } var useDirectDns = rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag diff --git a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs index 9c559a23..d53b960d 100644 --- a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs @@ -8,6 +8,7 @@ public class DNSSettingViewModel : MyReactiveObject [Reactive] public bool? BlockBindingQuery { get; set; } [Reactive] public string? DirectDNS { get; set; } [Reactive] public string? RemoteDNS { get; set; } + [Reactive] public string? BootstrapDNS { get; set; } [Reactive] public string? RayStrategy4Freedom { get; set; } [Reactive] public string? SingboxStrategy4Direct { get; set; } [Reactive] public string? SingboxStrategy4Proxy { get; set; } @@ -68,6 +69,7 @@ public class DNSSettingViewModel : MyReactiveObject BlockBindingQuery = item.BlockBindingQuery; DirectDNS = item.DirectDNS; RemoteDNS = item.RemoteDNS; + BootstrapDNS = item.BootstrapDNS; RayStrategy4Freedom = item.RayStrategy4Freedom; SingboxStrategy4Direct = item.SingboxStrategy4Direct; SingboxStrategy4Proxy = item.SingboxStrategy4Proxy; @@ -97,6 +99,7 @@ public class DNSSettingViewModel : MyReactiveObject _config.SimpleDNSItem.BlockBindingQuery = BlockBindingQuery; _config.SimpleDNSItem.DirectDNS = DirectDNS; _config.SimpleDNSItem.RemoteDNS = RemoteDNS; + _config.SimpleDNSItem.BootstrapDNS = BootstrapDNS; _config.SimpleDNSItem.RayStrategy4Freedom = RayStrategy4Freedom; _config.SimpleDNSItem.SingboxStrategy4Direct = SingboxStrategy4Direct; _config.SimpleDNSItem.SingboxStrategy4Proxy = SingboxStrategy4Proxy; diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml index d15929eb..e5a785b7 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml @@ -83,58 +83,79 @@ VerticalAlignment="Center" Text="{x:Static resx:ResUI.TbRemoteDNSTips}" TextWrapping="Wrap" /> - + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs index 71c3999c..a37f2c14 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs @@ -20,6 +20,7 @@ public partial class DNSSettingWindow : WindowBase cmbSBRemoteDNSStrategy.ItemsSource = Global.SingboxDomainStrategy4Out; cmbDirectDNS.ItemsSource = Global.DomainDirectDNSAddress; cmbRemoteDNS.ItemsSource = Global.DomainRemoteDNSAddress; + cmbBootstrapDNS.ItemsSource = Global.DomainPureIPDNSAddress; cmbDirectExpectedIPs.ItemsSource = Global.ExpectedIPs; cmbdomainStrategy4FreedomCompatible.ItemsSource = Global.DomainStrategy4Freedoms; @@ -35,6 +36,7 @@ public partial class DNSSettingWindow : WindowBase this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).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.BootstrapDNS, v => v.cmbBootstrapDNS.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); diff --git a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml index 733f5a97..7deb7c25 100644 --- a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml +++ b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml @@ -115,10 +115,34 @@ Margin="{StaticResource Margin8}" VerticalAlignment="Center" Style="{StaticResource ToolbarTextBlock}" + Text="{x:Static resx:ResUI.TbBootstrapDNS}" /> + + + + diff --git a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs index ad99b357..3eabd26f 100644 --- a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs @@ -18,6 +18,7 @@ public partial class DNSSettingWindow cmbSBRemoteDNSStrategy.ItemsSource = Global.SingboxDomainStrategy4Out; cmbDirectDNS.ItemsSource = Global.DomainDirectDNSAddress; cmbRemoteDNS.ItemsSource = Global.DomainRemoteDNSAddress; + cmbBootstrapDNS.ItemsSource = Global.DomainPureIPDNSAddress; cmbDirectExpectedIPs.ItemsSource = Global.ExpectedIPs; cmbdomainStrategy4FreedomCompatible.ItemsSource = Global.DomainStrategy4Freedoms; @@ -33,6 +34,7 @@ public partial class DNSSettingWindow this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).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.BootstrapDNS, v => v.cmbBootstrapDNS.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.RayStrategy4Freedom, v => v.cmbRayFreedomDNSStrategy.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.Text).DisposeWith(disposables);