Compare commits

...

2 commits

Author SHA1 Message Date
DHR60
d9843dc775
Add DNS Hosts features (#8756)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release Linux / rpm (push) Blocked by required conditions
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2026-02-05 17:05:27 +08:00
2dust
bceebc1661 Add process to routing domains
https://github.com/2dust/v2rayN/issues/8757
2026-02-05 16:52:48 +08:00
8 changed files with 89 additions and 55 deletions

View file

@ -332,22 +332,20 @@ public class Utils
.ToList(); .ToList();
} }
public static Dictionary<string, List<string>> ParseHostsToDictionary(string hostsContent) public static Dictionary<string, List<string>> ParseHostsToDictionary(string? hostsContent)
{ {
if (hostsContent.IsNullOrEmpty())
{
return new();
}
var userHostsMap = hostsContent var userHostsMap = hostsContent
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim()) .Select(line => line.Trim())
// skip full-line comments // skip full-line comments
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#")) .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;
})
// ensure line still contains valid parts // ensure line still contains valid parts
.Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' ')) .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) .Where(parts => parts.Length >= 2)
.GroupBy(parts => parts[0]) .GroupBy(parts => parts[0])
.ToDictionary( .ToDictionary(
@ -1086,7 +1084,19 @@ public class Utils
public static string GetExeName(string name) public static string GetExeName(string name)
{ {
return IsWindows() ? $"{name}.exe" : name; if (name.IsNullOrEmpty() || IsNonWindows())
{
return name;
}
if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
return name;
}
else
{
return $"{name}.exe";
}
} }
public static bool IsAdministrator() public static bool IsAdministrator()

View file

@ -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); hostsDns.predefined[kvp.Key] = kvp.Value.Where(s => Utils.IsIpAddress(s)).ToList();
foreach (var kvp in userHostsMap)
{
hostsDns.predefined[kvp.Key] = kvp.Value;
}
} }
foreach (var host in hostsDns.predefined) foreach (var host in hostsDns.predefined)
@ -115,7 +110,7 @@ public partial class CoreConfigSingboxService
} }
singboxConfig.dns ??= new Dns4Sbox(); singboxConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.servers ??= new List<Server4Sbox>(); singboxConfig.dns.servers ??= [];
singboxConfig.dns.servers.Add(remoteDns); singboxConfig.dns.servers.Add(remoteDns);
singboxConfig.dns.servers.Add(directDns); singboxConfig.dns.servers.Add(directDns);
singboxConfig.dns.servers.Add(hostsDns); 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); var (ech, _) = ParseEchParam(node?.EchConfigList);
if (ech is not null) if (ech is not null)
{ {
var echDomain = ech.query_server_name ?? node?.Sni; var echDomain = ech.query_server_name ?? node?.Sni;
singboxConfig.dns.rules.Add(new() singboxConfig.dns.rules.Add(new()
{ {
query_type = new List<int> { 64, 65 }, query_type = [64, 65],
server = Global.SingboxEchDNSTag, server = Global.SingboxEchDNSTag,
domain = echDomain is not null ? new List<string> { echDomain } : null, domain = echDomain is not null ? new List<string> { echDomain } : null,
}); });
@ -209,7 +247,7 @@ public partial class CoreConfigSingboxService
{ {
singboxConfig.dns.rules.Add(new() singboxConfig.dns.rules.Add(new()
{ {
query_type = new List<int> { 64, 65 }, query_type = [64, 65],
server = Global.SingboxEchDNSTag, server = Global.SingboxEchDNSTag,
domain = queryServerNames, domain = queryServerNames,
}); });
@ -220,9 +258,9 @@ public partial class CoreConfigSingboxService
{ {
singboxConfig.dns.rules.Add(new() singboxConfig.dns.rules.Add(new()
{ {
query_type = new List<int> { 64, 65 }, query_type = [64, 65],
action = "predefined", action = "predefined",
rcode = "NOTIMP" rcode = "NOERROR"
}); });
} }
@ -236,13 +274,14 @@ public partial class CoreConfigSingboxService
type = "logical", type = "logical",
mode = "and", mode = "and",
rewrite_ttl = 1, rewrite_ttl = 1,
rules = new List<Rule4Sbox> rules =
{ [
new() { new()
query_type = new List<int> { 1, 28 }, // A and AAAA {
query_type = [1, 28], // A and AAAA
}, },
fakeipFilterRule, fakeipFilterRule
} ]
}; };
singboxConfig.dns.rules.Add(rule4Fake); singboxConfig.dns.rules.Add(rule4Fake);
@ -262,7 +301,7 @@ public partial class CoreConfigSingboxService
if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs)) if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs))
{ {
var ipItems = simpleDNSItem.DirectExpectedIPs var ipItems = simpleDNSItem.DirectExpectedIPs
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) .Split([',', ';'], StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim()) .Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s)) .Where(s => !string.IsNullOrEmpty(s))
.ToList(); .ToList();

View file

@ -75,14 +75,8 @@ public partial class CoreConfigSingboxService
var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (dnsItem == null || !dnsItem.Enabled) if (dnsItem == null || !dnsItem.Enabled)
{ {
if (!simpleDnsItem.Hosts.IsNullOrEmpty()) var userHostsMap = Utils.ParseHostsToDictionary(simpleDnsItem.Hosts);
{ hostsDomains.AddRange(userHostsMap.Select(kvp => kvp.Key));
var userHostsMap = Utils.ParseHostsToDictionary(simpleDnsItem.Hosts);
foreach (var kvp in userHostsMap)
{
hostsDomains.Add(kvp.Key);
}
}
if (simpleDnsItem.UseSystemHosts == true) if (simpleDnsItem.UseSystemHosts == true)
{ {
var systemHostsMap = Utils.GetSystemHosts(); var systemHostsMap = Utils.GetSystemHosts();
@ -277,7 +271,7 @@ public partial class CoreConfigSingboxService
} }
} }
if (_config.TunModeItem.EnableTun && item.Process?.Count > 0) if (item.Process?.Count > 0)
{ {
var ruleProcName = JsonUtils.DeepCopy(rule3); var ruleProcName = JsonUtils.DeepCopy(rule3);
ruleProcName.process_name ??= []; ruleProcName.process_name ??= [];
@ -304,11 +298,7 @@ public partial class CoreConfigSingboxService
} }
// sing-box strictly matches the exe suffix on Windows // sing-box strictly matches the exe suffix on Windows
var procName = process; var procName = Utils.GetExeName(process);
if (Utils.IsWindows() && !procName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
procName += ".exe";
}
ruleProcName.process_name.Add(procName); ruleProcName.process_name.Add(procName);
} }

View file

@ -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); dnsItem.hosts[kvp.Key] = kvp.Value;
foreach (var kvp in userHostsMap)
{
dnsItem.hosts[kvp.Key] = kvp.Value;
}
} }
return await Task.FromResult(0); return await Task.FromResult(0);
} }

View file

@ -109,7 +109,7 @@ public partial class CoreConfigV2rayService
v2rayConfig.routing.rules.Add(it); v2rayConfig.routing.rules.Add(it);
hasDomainIp = true; hasDomainIp = true;
} }
if (_config.TunModeItem.EnableTun && rule.process?.Count > 0) if (rule.process?.Count > 0)
{ {
var it = JsonUtils.DeepCopy(rule); var it = JsonUtils.DeepCopy(rule);
it.domain = null; it.domain = null;

View file

@ -106,7 +106,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
Network = item.Network, Network = item.Network,
Protocols = Utils.List2String(item.Protocol), Protocols = Utils.List2String(item.Protocol),
InboundTags = Utils.List2String(item.InboundTag), InboundTags = Utils.List2String(item.InboundTag),
Domains = Utils.List2String((item.Domain ?? []).Concat(item.Ip ?? []).ToList()), Domains = Utils.List2String((item.Domain ?? []).Concat(item.Ip ?? []).ToList().Concat(item.Process ?? []).ToList()),
Enabled = item.Enabled, Enabled = item.Enabled,
Remarks = item.Remarks, Remarks = item.Remarks,
}; };

View file

@ -248,7 +248,7 @@
<DataGridTextColumn <DataGridTextColumn
Width="*" Width="*"
Binding="{Binding Domains}" Binding="{Binding Domains}"
Header="domain / ip" /> Header="domain / ip / process" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</TabItem> </TabItem>

View file

@ -333,7 +333,7 @@
<DataGridTextColumn <DataGridTextColumn
Width="*" Width="*"
Binding="{Binding Domains}" Binding="{Binding Domains}"
Header="domain / ip" /> Header="domain / ip / process" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</TabItem> </TabItem>