Skip to content
Merged
512 changes: 261 additions & 251 deletions Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Source/NETworkManager.Localization/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -4496,4 +4496,7 @@ Cloudflare may log your IP address and network information. See Cloudflare's pri
<data name="Stop" xml:space="preserve">
<value>Stop</value>
</data>
<data name="NotSet" xml:space="preserve">
<value>Not set</value>
</data>
</root>
27 changes: 15 additions & 12 deletions Source/NETworkManager.Models/Network/DNSLookup.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using DnsClient;
using DnsClient.Protocol;
using NETworkManager.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Threading.Tasks;

namespace NETworkManager.Models.Network;
Expand All @@ -24,7 +24,7 @@ public DNSLookup(DNSLookupSettings settings, IEnumerable<ServerConnectionInfo> d
{
_suffix = _settings.UseCustomDNSSuffix
? _settings.CustomDNSSuffix
: IPGlobalProperties.GetIPGlobalProperties().DomainName;
: DNSClientHelper.DetectDNSSuffix();

_addSuffix = !string.IsNullOrEmpty(_suffix);
}
Expand Down Expand Up @@ -108,25 +108,28 @@ private void OnLookupComplete()
/// <returns>List of DNS servers as <see cref="IPEndPoint" />.</returns>
private IEnumerable<IPEndPoint> GetDnsServer(IEnumerable<ServerConnectionInfo> dnsServers = null)
{
List<IPEndPoint> servers = [];
// Use Windows dns servers
List<IPEndPoint> servers =
[
.. dnsServers == null
? NameServer.ResolveNameServers(true, false).Select(dnsServer =>
new IPEndPoint(IPAddress.Parse(dnsServer.Address), dnsServer.Port))
: dnsServers.Select(dnsServer => new IPEndPoint(IPAddress.Parse(dnsServer.Server), dnsServer.Port))

// Use windows dns servers
servers.AddRange(dnsServers == null
? NameServer.ResolveNameServers(true, false).Select(dnsServer =>
new IPEndPoint(IPAddress.Parse(dnsServer.Address), dnsServer.Port))
: dnsServers.Select(dnsServer => new IPEndPoint(IPAddress.Parse(dnsServer.Server), dnsServer.Port)));
];

return servers;
}

/// <summary>
/// Append DNS suffix to hostname if not set.
/// Append DNS suffix to hostname if not set. IP literals are left unchanged (the Host input
/// accepts hostnames and IP addresses alike, for any query type).
/// </summary>
/// <param name="hosts">List of hosts</param>
/// <returns>List of host with DNS suffix</returns>
private IEnumerable<string> GetHostWithSuffix(IEnumerable<string> hosts)
private IEnumerable<string> GetHostsWithSuffix(IEnumerable<string> hosts)
{
return hosts.Select(host => host.Contains('.') ? host : $"{host}.{_suffix}").ToList();
return [.. hosts.Select(host => DNSClientHelper.IsBareHostname(host) ? $"{host}.{_suffix}" : host)];
}

/// <summary>
Expand All @@ -138,7 +141,7 @@ public void ResolveAsync(IEnumerable<string> hosts)
Task.Run(() =>
{
// Append dns suffix to hostname, if the option is set, otherwise copy the list
var queries = _addSuffix && _settings.QueryType != QueryType.PTR ? GetHostWithSuffix(hosts) : hosts;
var queries = _addSuffix && _settings.QueryType != QueryType.PTR ? GetHostsWithSuffix(hosts) : hosts;

// For each dns server
Parallel.ForEach(_servers, dnsServer =>
Expand Down
9 changes: 6 additions & 3 deletions Source/NETworkManager.Models/Network/ServerConnectionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,14 @@ public static ServerConnectionInfo Parse(string input, int defaultPort, Transpor
}

/// <summary>
/// Returns a string that represents the current object.
/// Returns a string that represents the current object. IPv6 addresses are bracketed
/// (<c>[::1]:53</c>) so the address and port remain distinguishable.
/// </summary>
/// <returns>Server:Port</returns>
/// <returns>Server:Port, or [Server]:Port for an IPv6 address.</returns>
public override string ToString()
{
return $"{Server}:{Port}";
return IPAddress.TryParse(Server, out var ip) && ip.AddressFamily == AddressFamily.InterNetworkV6
? $"[{Server}]:{Port}"
: $"{Server}:{Port}";
}
}
4 changes: 4 additions & 0 deletions Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public static class GlobalStaticConfiguration
// Network config
public static int NetworkChangeDetectionDelay => 5000;

// Delay before applying DNS server settings change.
public static TimeSpan NetworkConfigApplyDispatcherTimerTimeSpan => new(0, 0, 0, 2, 500);

// Notification config
// Minimum interval (ms) between two notification sounds, so a burst of near-simultaneous
// status changes collapses into a single sound instead of an overlapping cacophony.
Expand Down Expand Up @@ -72,6 +75,7 @@ public static class GlobalStaticConfiguration
public static bool Appearance_UseCustomTheme => false;

// Settings: Network
public static bool Network_AddDNSSuffix => true;
public static bool Network_ResolveHostnamePreferIPv4 => true;

// Settings: Status
Expand Down
54 changes: 54 additions & 0 deletions Source/NETworkManager.Settings/SettingsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,8 @@ public bool Network_UseCustomDNSServer
}
}

[Obsolete("Use Network_CustomDNSServers instead.")]
[field: Obsolete("Use Network_CustomDNSServers instead.")]
public string Network_CustomDNSServer
{
get;
Expand All @@ -387,6 +389,58 @@ public string Network_CustomDNSServer
}
}

public ObservableCollection<ServerConnectionInfo> Network_CustomDNSServers
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
} = [];

public bool Network_AddDNSSuffix
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
} = GlobalStaticConfiguration.Network_AddDNSSuffix;

public bool Network_UseCustomDNSSuffix
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
}

public string Network_CustomDNSSuffix
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
}

public bool Network_ResolveHostnamePreferIPv4
{
get;
Expand Down
23 changes: 23 additions & 0 deletions Source/NETworkManager.Settings/SettingsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using NETworkManager.Models.Network;
using NETworkManager.Utilities;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.Json;
Expand Down Expand Up @@ -638,7 +639,9 @@ private static void UpgradeTo_2026_2_22_0()
// DNS Lookup
Log.Info("Migrate DNS Lookup settings to new structure...");

#pragma warning disable CS0618 // Type or member is obsolete
Current.DNSLookup_SelectedDNSServer_v2 = Current.DNSLookup_SelectedDNSServer?.Name;
#pragma warning restore CS0618 // Type or member is obsolete

Log.Info($"Selected DNS server set to \"{Current.DNSLookup_SelectedDNSServer_v2}\"");

Expand Down Expand Up @@ -788,6 +791,26 @@ private static void UpgradeToLatest(Version version)
Log.Info($"Add \"{portProfile.Name}\" to \"PortScanner_PortProfiles\"...");
Current.PortScanner_PortProfiles.Add(portProfile);
}

// Migrate custom DNS servers from a semicolon-separated string (no port) to a list of ServerConnectionInfo
#pragma warning disable CS0618
if (!string.IsNullOrEmpty(Current.Network_CustomDNSServer))
{
Log.Info("Migrate custom DNS servers to new structure...");

Current.Network_CustomDNSServers =
[
.. Current
.Network_CustomDNSServer
.Split(';')
.Select(server => server.Trim())
.Where(server => server.Length > 0)
.Select(server => new ServerConnectionInfo(server, 53, TransportProtocol.Udp))
];

Current.Network_CustomDNSServer = string.Empty; // Clear the old property to avoid multiple migrations with pre-release versions
}
#pragma warning restore CS0618
}
#endregion
}
71 changes: 47 additions & 24 deletions Source/NETworkManager.Utilities/DNSClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,66 +18,67 @@ public class DNSClient : SingletonBase<DNSClient>
private const string NotConfiguredMessage = "DNS client is not configured. Call Configure() first.";

/// <summary>
/// Hold the current instance of the LookupClient.
/// Immutable snapshot of everything a resolve call needs (lookup client + settings it was configured
/// with), published as a single reference so concurrent resolves during a <see cref="Configure" />
/// call never observe a torn combination (e.g. the old suffix flag with the new lookup client).
/// Relies on <see cref="DNSClientSettings" /> not being mutated after being passed to
/// <see cref="Configure" /> - see the note on <see cref="DNSClientSettings" /> itself.
/// </summary>
private LookupClient _client;
private sealed record ResolverState(LookupClient Client, bool AddSuffix, DNSClientSettings Settings);

/// <summary>
/// Indicates if the DNS client is configured.
/// </summary>
private bool _isConfigured;

/// <summary>
/// Store the current DNS settings.
/// Current resolver state (lookup client + DNS suffix behavior), swapped atomically on configure.
/// </summary>
private DNSClientSettings _settings;
private ResolverState _state;

/// <summary>
/// Method to configure the DNS client.
/// </summary>
/// <param name="settings"></param>
public void Configure(DNSClientSettings settings)
{
_settings = settings;

Log.Debug("Configure - Configuring DNS client...");

if (_settings.UseCustomDNSServers)
LookupClient client;

if (settings.UseCustomDNSServers)
{
Log.Debug("Configure - Using custom DNS servers...");

// Setup custom DNS servers
List<NameServer> servers = [];

foreach (var (server, port) in _settings.DNSServers)
foreach (var (server, port) in settings.DNSServers)
{
Log.Debug($"Configure - Adding custom DNS server: {server}:{port}");
servers.Add(new IPEndPoint(IPAddress.Parse(server), port));
}

Log.Debug("Configure - Creating LookupClient with custom DNS servers...");
_client = new LookupClient(new LookupClientOptions(servers.ToArray()));
client = new LookupClient(new LookupClientOptions([.. servers]));
}
else
{
Log.Debug("Configure - Creating LookupClient with Windows default DNS servers...");
_client = new LookupClient();
client = new LookupClient();
}

var addSuffix = settings.AddDNSSuffix && !string.IsNullOrEmpty(settings.DNSSuffix);
Log.Debug(addSuffix
? $"Configure - DNS suffix will be added to hostnames without a dot: {settings.DNSSuffix}"
: "Configure - DNS suffix will NOT be added to hostnames without a dot.");

_state = new ResolverState(client, addSuffix, settings);

Log.Debug("Configure - DNS client configured.");
_isConfigured = true;
}

/// <summary>
/// Method to update the (Windows) name servers of the DNS client
/// when they may have changed due to a network update.
/// </summary>
public void UpdateWindowsDNSSever()
{
Log.Debug("UpdateWindowsDNSSever - Recreating LookupClient with with Windows default DNS servers...");
_client = new LookupClient();
}

/// <summary>
/// Resolve an IPv4 address from a hostname or FQDN.
/// </summary>
Expand All @@ -88,9 +89,13 @@ public async Task<DNSClientResultIPAddress> ResolveAAsync(string query)
if (!_isConfigured)
throw new DNSClientNotConfiguredException(NotConfiguredMessage);

var state = _state;

query = AddDNSSuffixIfConfigured(query, state);

try
{
var result = await _client.QueryAsync(query, QueryType.A);
var result = await state.Client.QueryAsync(query, QueryType.A);

// Pass the error we got from the lookup client (dns server).
// NXDOMAIN is not a real failure like a timeout - flag it via IsNotFound so callers can
Expand Down Expand Up @@ -131,9 +136,13 @@ public async Task<DNSClientResultIPAddress> ResolveAaaaAsync(string query)
if (!_isConfigured)
throw new DNSClientNotConfiguredException(NotConfiguredMessage);

var state = _state;

query = AddDNSSuffixIfConfigured(query, state);

try
{
var result = await _client.QueryAsync(query, QueryType.AAAA);
var result = await state.Client.QueryAsync(query, QueryType.AAAA);

// Pass the error we got from the lookup client (dns server).
// NXDOMAIN is not a real failure like a timeout - flag it via IsNotFound so callers can
Expand Down Expand Up @@ -176,7 +185,7 @@ public async Task<DNSClientResultString> ResolveCnameAsync(string query)

try
{
var result = await _client.QueryAsync(query, QueryType.CNAME);
var result = await _state.Client.QueryAsync(query, QueryType.CNAME);

// Pass the error we got from the lookup client (dns server).
// NXDOMAIN is not a real failure like a timeout - flag it via IsNotFound so callers can
Expand Down Expand Up @@ -219,7 +228,7 @@ public async Task<DNSClientResultString> ResolvePtrAsync(IPAddress ipAddress)

try
{
var result = await _client.QueryReverseAsync(ipAddress);
var result = await _state.Client.QueryReverseAsync(ipAddress);

// Pass the error we got from the lookup client (dns server).
// NXDOMAIN is always a clean "no record". For private/ULA IP ranges (the common case for
Expand Down Expand Up @@ -255,6 +264,20 @@ public async Task<DNSClientResultString> ResolvePtrAsync(IPAddress ipAddress)
}
}

/// <summary>
/// Appends the configured DNS suffix to a bare hostname (forward lookups only).
/// FQDNs and IP literals are returned unchanged.
/// </summary>
/// <param name="query">Hostname, FQDN, or IP address as string like "example.com".</param>
/// <param name="state">Resolver state snapshot captured at the start of the resolve call.</param>
/// <returns>Query with the DNS suffix appended, if configured and applicable.</returns>
private static string AddDNSSuffixIfConfigured(string query, ResolverState state)
{
return state.AddSuffix && DNSClientHelper.IsBareHostname(query)
? $"{query}.{state.Settings.DNSSuffix}"
: query;
}

/// <summary>
/// Determines whether a DNS response code means "no record" rather than a real failure.
/// NXDOMAIN is always a clean "does not exist". SERVFAIL and REFUSED are only treated as
Expand Down
Loading
Loading