From d9843dc77502454b1ec48cec6244e115f1abd082 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Thu, 5 Feb 2026 09:05:27 +0000 Subject: [PATCH] Add DNS Hosts features (#8756) --- v2rayN/ServiceLib/Common/Utils.cs | 18 ++--- .../CoreConfig/Singbox/SingboxDnsService.cs | 77 ++++++++++++++----- .../Singbox/SingboxRoutingService.cs | 10 +-- .../CoreConfig/V2ray/V2rayDnsService.cs | 9 +-- 4 files changed, 70 insertions(+), 44 deletions(-) diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index 7a3fa9a0..c538f9c8 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -332,22 +332,20 @@ public class Utils .ToList(); } - public static Dictionary> ParseHostsToDictionary(string hostsContent) + public static Dictionary> ParseHostsToDictionary(string? hostsContent) { + if (hostsContent.IsNullOrEmpty()) + { + return new(); + } var userHostsMap = hostsContent - .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) .Select(line => line.Trim()) // skip full-line comments - .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#")) - // strip inline comments (truncate at '#') - .Select(line => - { - var index = line.IndexOf('#'); - return index >= 0 ? line.Substring(0, index).Trim() : line; - }) + .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith('#')) // ensure line still contains valid parts .Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' ')) - .Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)) + .Select(line => line.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries)) .Where(parts => parts.Length >= 2) .GroupBy(parts => parts[0]) .ToDictionary( diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index 6079b2fb..7684cbd1 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -88,14 +88,9 @@ public partial class CoreConfigSingboxService } } - if (!simpleDNSItem.Hosts.IsNullOrEmpty()) + foreach (var kvp in Utils.ParseHostsToDictionary(simpleDNSItem.Hosts)) { - var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts); - - foreach (var kvp in userHostsMap) - { - hostsDns.predefined[kvp.Key] = kvp.Value; - } + hostsDns.predefined[kvp.Key] = kvp.Value.Where(s => Utils.IsIpAddress(s)).ToList(); } foreach (var host in hostsDns.predefined) @@ -115,7 +110,7 @@ public partial class CoreConfigSingboxService } singboxConfig.dns ??= new Dns4Sbox(); - singboxConfig.dns.servers ??= new List(); + singboxConfig.dns.servers ??= []; singboxConfig.dns.servers.Add(remoteDns); singboxConfig.dns.servers.Add(directDns); singboxConfig.dns.servers.Add(hostsDns); @@ -191,13 +186,56 @@ public partial class CoreConfigSingboxService } }); + foreach (var kvp in Utils.ParseHostsToDictionary(simpleDNSItem.Hosts)) + { + var predefined = kvp.Value.First(); + if (predefined.IsNullOrEmpty() || Utils.IsIpAddress(predefined)) + { + continue; + } + if (predefined.StartsWith('#') && int.TryParse(predefined.AsSpan(1), out var rcode)) + { + // xray syntactic sugar for predefined + // etc. #0 -> NOERROR + singboxConfig.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", + rcode = "NOERROR", + answer = [$"*. IN CNAME {predefined}."], + }; + if (ParseV2Domain(kvp.Key, rule)) + { + singboxConfig.dns.rules.Add(rule); + } + } + var (ech, _) = ParseEchParam(node?.EchConfigList); if (ech is not null) { var echDomain = ech.query_server_name ?? node?.Sni; singboxConfig.dns.rules.Add(new() { - query_type = new List { 64, 65 }, + query_type = [64, 65], server = Global.SingboxEchDNSTag, domain = echDomain is not null ? new List { echDomain } : null, }); @@ -209,7 +247,7 @@ public partial class CoreConfigSingboxService { singboxConfig.dns.rules.Add(new() { - query_type = new List { 64, 65 }, + query_type = [64, 65], server = Global.SingboxEchDNSTag, domain = queryServerNames, }); @@ -220,9 +258,9 @@ public partial class CoreConfigSingboxService { singboxConfig.dns.rules.Add(new() { - query_type = new List { 64, 65 }, + query_type = [64, 65], action = "predefined", - rcode = "NOTIMP" + rcode = "NOERROR" }); } @@ -236,13 +274,14 @@ public partial class CoreConfigSingboxService type = "logical", mode = "and", rewrite_ttl = 1, - rules = new List - { - new() { - query_type = new List { 1, 28 }, // A and AAAA + rules = + [ + new() + { + query_type = [1, 28], // A and AAAA }, - fakeipFilterRule, - } + fakeipFilterRule + ] }; singboxConfig.dns.rules.Add(rule4Fake); @@ -262,7 +301,7 @@ public partial class CoreConfigSingboxService if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs)) { var ipItems = simpleDNSItem.DirectExpectedIPs - .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) + .Split([',', ';'], StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) .Where(s => !string.IsNullOrEmpty(s)) .ToList(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 0370ce29..6198d027 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -75,14 +75,8 @@ public partial class CoreConfigSingboxService var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); if (dnsItem == null || !dnsItem.Enabled) { - if (!simpleDnsItem.Hosts.IsNullOrEmpty()) - { - var userHostsMap = Utils.ParseHostsToDictionary(simpleDnsItem.Hosts); - foreach (var kvp in userHostsMap) - { - hostsDomains.Add(kvp.Key); - } - } + var userHostsMap = Utils.ParseHostsToDictionary(simpleDnsItem.Hosts); + hostsDomains.AddRange(userHostsMap.Select(kvp => kvp.Key)); if (simpleDnsItem.UseSystemHosts == true) { var systemHostsMap = Utils.GetSystemHosts(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs index 2329c395..7de97240 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -333,14 +333,9 @@ public partial class CoreConfigV2rayService } } - if (!simpleDNSItem.Hosts.IsNullOrEmpty()) + foreach (var kvp in Utils.ParseHostsToDictionary(simpleDNSItem.Hosts)) { - var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts); - - foreach (var kvp in userHostsMap) - { - dnsItem.hosts[kvp.Key] = kvp.Value; - } + dnsItem.hosts[kvp.Key] = kvp.Value; } return await Task.FromResult(0); }