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);