From 904ae6ba6cd7ed9e3e716abc4517e60381c248c2 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:20:17 +0200 Subject: [PATCH 01/13] Fix: IP Scanner icon --- Source/NETworkManager/Views/IPScannerView.xaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/NETworkManager/Views/IPScannerView.xaml b/Source/NETworkManager/Views/IPScannerView.xaml index d328b7eb8f..36e784063d 100644 --- a/Source/NETworkManager/Views/IPScannerView.xaml +++ b/Source/NETworkManager/Views/IPScannerView.xaml @@ -530,7 +530,10 @@ - + Date: Sun, 6 Sep 2026 23:24:28 +0200 Subject: [PATCH 02/13] Feature: Custom dns suffix --- HANDOVER.md | 70 +++++++++++++++++++ .../GlobalStaticConfiguration.cs | 1 + .../NETworkManager.Settings/SettingsInfo.cs | 54 ++++++++++++++ .../SettingsManager.cs | 23 ++++++ .../MultipleIPAddressesValidator.cs | 27 ------- 5 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 HANDOVER.md delete mode 100644 Source/NETworkManager.Validators/MultipleIPAddressesValidator.cs diff --git a/HANDOVER.md b/HANDOVER.md new file mode 100644 index 0000000000..2b18b8d5d7 --- /dev/null +++ b/HANDOVER.md @@ -0,0 +1,70 @@ +# Handover: Global DNS suffix + custom DNS server rework + +**Status:** Implemented, uncommitted, NOT built/tested (no Windows/.NET SDK in the sandbox that did this work). +**Branch:** `feature/3559` (clean branch off `main` @ `38989cb22`, no commits yet — all changes below are working-tree edits only). +**This file is untracked** (not added to git) — delete it once you're done, or `git add`/commit it if you want it kept. + +## What was asked + +Extend the global Network settings (`SettingsNetworkViewModel`/`SettingsNetworkView`) to add two things that already exist in the DNS Lookup tool's own settings, making them apply to *all* tools that resolve hostnames (Ping, Traceroute, PortScanner, IPScanner, Connection, NTP Lookup, etc.), not just DNS Lookup: + +1. **DNS suffix support** — "Add DNS suffix (primary) to hostname" + "Use custom DNS suffix" + the suffix textbox. +2. **Rework of the custom DNS server input** — was a single semicolon-separated IP-only textbox; wanted something closer to DNS Lookup's server dialog (label + Edit button instead of a raw textbox). + +## Research findings (still true, useful context) + +- There are **two independent DNS resolution paths** in the codebase: + - The shared singleton `NETworkManager.Utilities.DNSClient`, configured once from `MainWindow.ConfigureDNSServer()` off the `Network_*` settings. This is the resolver behind PTR (reverse) lookups in Ping/Traceroute/PortScanner/IPScanner/Connection/NetworkConnectionWidget, **and** the forward A/AAAA chokepoint `DNSClientHelper.ResolveAorAaaaAsync` used by `HostRangeHelper` (host-range parsing shared by Ping/Traceroute/PortScanner/IPScanner) and `SNTPLookup`. This is the one place that needed suffix support to make it "global." + - DNS Lookup's own independent path in `NETworkManager.Models.Network.DNSLookup.cs`, which builds a fresh `LookupClient` per query and never touches the shared singleton. **Not touched by this change** — it already had its own suffix logic (`DNSLookup_AddDNSSuffix`/`DNSLookup_UseCustomDNSSuffix`/`DNSLookup_CustomDNSSuffix`), which was the template copied for the global settings. +- DNS Lookup's server-editing dialog (`ServerConnectionInfoProfileChildWindow` + `ServerConnectionInfoProfileViewModel`) is a **named-profile** editor (Name + list of Server:Port). Global settings only need one unnamed list, so it's reused with a new `isNameReadOnly` flag rather than building a new dialog. +- `DNSClient.Configure()` calls `IPAddress.Parse(server)` directly on custom DNS server entries — **custom DNS servers must stay IP-only** (hostnames would throw). DNS Lookup's dialog allows hostnames for its own servers; ours is opened with `allowOnlyIPAddress: true`. + +## Decisions made (confirmed with user via AskUserQuestion) + +1. **"Add DNS suffix" defaults to ON** (matches DNS Lookup's own default), even though this changes hostname-resolution behavior for existing users across Ping/Traceroute/PortScanner/IPScanner/SNTP after upgrade. +2. **Custom DNS server editing reuses the DNS Lookup profile dialog** (`ServerConnectionInfoProfileChildWindow`/`ServerConnectionInfoProfileViewModel`), opened with the profile name fixed to `Strings.DNSServers` ("DNS server(s)") and **read-only** (new `IsNameReadOnly` property/binding), since there's only one global list, not multiple named profiles. +3. **Per-server port is configurable** (previously hardcoded to 53). DnsClient.net already supports arbitrary `IPEndPoint` per server, so this was low cost. + +## Files changed (all uncommitted) + +| File | Change | +|---|---| +| `Source/NETworkManager.Settings/GlobalStaticConfiguration.cs` | Added `Network_AddDNSSuffix => true` default. **Note:** a linter/format pass (not me) reordered this line to sit alphabetically above `Network_ResolveHostnamePreferIPv4` — that reorder is intentional, don't revert it. | +| `Source/NETworkManager.Settings/SettingsInfo.cs` | Added `Network_CustomDNSServers` (`ObservableCollection`), `Network_AddDNSSuffix`, `Network_UseCustomDNSSuffix`, `Network_CustomDNSSuffix`. Marked old `Network_CustomDNSServer` (string) `[Obsolete]` — kept only for the migration below. | +| `Source/NETworkManager.Settings/SettingsManager.cs` | New upgrade step `UpgradeTo_2026_8_17_0()` (registered in the `Upgrade()` dispatcher chain) migrates the old semicolon-separated `Network_CustomDNSServer` string into `Network_CustomDNSServers` at port 53/UDP, preserving current behavior for upgraders. **Version number `2026.8.17.0` was a placeholder guess (today's date) — confirm/adjust to match whatever version this actually ships in**, following the project's `yyyy.M.d.0` date-based versioning (see `AGENTS.md`). | +| `Source/NETworkManager.Utilities/DNSClientSettings.cs` | Added `AddDNSSuffix` (bool) and `DNSSuffix` (string, already-resolved value) fields. | +| `Source/NETworkManager.Utilities/DNSClient.cs` | `Configure()` precomputes `_addSuffix`. New private `AddDNSSuffixIfConfigured(query)` appends `.{suffix}` to hostnames without a dot; called from `ResolveAAsync`/`ResolveAaaaAsync` only (not PTR/CNAME — matches `DNSLookup.cs`'s behavior of skipping suffix for reverse lookups). | +| `Source/NETworkManager/MainWindow.xaml.cs` | `ConfigureDNSServer()` rewritten to build the server list from `Network_CustomDNSServers` (real per-entry ports) and to populate `AddDNSSuffix`/`DNSSuffix` on `DNSClientSettings` (custom suffix trimmed of leading `.`, else `IPGlobalProperties.GetIPGlobalProperties().DomainName`, mirroring `DNSLookupViewModel.QueryAsync()`). `SettingsManager_PropertyChanged` switch extended with cases for `Network_CustomDNSServers`, `Network_AddDNSSuffix`, `Network_UseCustomDNSSuffix`, `Network_CustomDNSSuffix` so live settings changes reconfigure DNS immediately (same as the pre-existing two cases). | +| `Source/NETworkManager/ViewModels/ServerConnectionInfoProfileViewModel.cs` | New optional ctor param `isNameReadOnly = false` → new bindable `IsNameReadOnly` property. | +| `Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml` | `TextBoxName.IsReadOnly` bound to `IsNameReadOnly`. | +| `Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml.cs` | `ChildWindow_OnLoaded` now focuses `TextBoxServer` instead of `TextBoxName` when the name is read-only (small UX polish so tab focus doesn't land on an uneditable field). | +| `Source/NETworkManager/ViewModels/SettingsNetworkViewModel.cs` | Rewritten. New: `CustomDNSServersDisplay` (read-only joined-string of `Network_CustomDNSServers`), `AddDNSSuffix`/`UseCustomDNSSuffix`/`CustomDNSSuffix` properties (copied pattern from `DNSLookupSettingsViewModel`), `EditCustomDNSServersCommand` → `EditCustomDNSServers()` which opens the reused profile dialog (see decisions above) and writes the result back to `Network_CustomDNSServers`. Old `CustomDNSServer` string property removed entirely. | +| `Source/NETworkManager/Views/SettingsNetworkView.xaml` | Old DNS server `TextBox` replaced with a label (`CustomDNSServersDisplay`) + Edit icon button (`EditCustomDNSServersCommand`, `iconPacks:Modern Kind=Edit`, tooltip `Strings.EditDNSServer`). Added suffix toggle/textbox section copied verbatim in structure from `DNSLookupSettingsView.xaml` (reuses existing localized strings — no new resx keys needed anywhere in this change). | +| `Source/NETworkManager.Validators/MultipleIPAddressesValidator.cs` | **Deleted** — was only referenced by the old DNS server textbox, now dead code. Confirmed via repo-wide grep before deleting. | +| `Website/docs/settings/network.md` | Updated to document the new "DNS server(s)" editing UX (now IP+port, edited via dialog) and the three new suffix settings. | + +## Verification status — IMPORTANT, not done yet + +**Nothing has been compiled or run.** This sandbox has no `dotnet` SDK and the project targets `net10.0-windows10.0.22621.0` (WPF, Windows-only), so it can't be built here at all. Verification so far was manual code tracing only: +- Confirmed no other files reference the deleted validator or the removed `CustomDNSServer` VM property (repo-wide grep). +- Confirmed `ServerConnectionInfo`/`TransportProtocol`/`ObservableCollection` patterns already exist and serialize fine elsewhere (`DNSLookup_DNSServers` uses the same shapes) — System.Text.Json settings persistence should just work. +- Confirmed `ServerValidator` genuinely enforces IP-only when `AllowOnlyIPAddress=true` (checked the validator source). +- Confirmed `DNSClientSettings` is only constructed in one place (`MainWindow.ConfigureDNSServer()`), so no other caller needed updating. + +**Next session should, in this order:** +1. **Build on Windows** (`dotnet build` or open in VS) and fix any compile errors — I could not verify this. +2. Manually test in the running app: + - Toggle "Use custom DNS server" → Edit button opens dialog with fixed read-only "DNS server(s)" name → add/edit/remove IP:port entries → Save → label updates, `Network_CustomDNSServers` persists across restart. + - Verify hostname-only entries are rejected in that dialog (IP-only enforcement). + - Toggle "Add DNS suffix (primary) to hostname" / "Use custom DNS suffix" + suffix textbox enable/disable interplay (mirrors DNS Lookup settings UI — should look/behave identically). + - Actually resolve a bare hostname (e.g. via Ping or IP Scanner host range) with suffix enabled and confirm the suffix gets appended and resolution works; confirm FQDNs and PTR/reverse lookups are unaffected. + - Test the settings-upgrade migration path: hand-edit/restore an old settings JSON with a populated `Network_CustomDNSServer` string and no `Network_CustomDNSServers`, bump the settings version below `2026.8.17.0`, launch, and confirm it migrates correctly into the new list at port 53. +3. **Confirm/adjust the migration version number** `2026.8.17.0` in `SettingsManager.cs` (`Upgrade()` dispatcher + `UpgradeTo_2026_8_17_0()`) to match the actual intended release version if it differs from today's date-based guess. +4. Consider whether `Website/docs/application/dns-lookup.md` or other doc pages should cross-reference the new global suffix setting (only `Website/docs/settings/network.md` was updated). +5. Nothing was committed — review the diff (`git diff`) and commit when satisfied. + +## Explicit scope notes (things deliberately NOT done) + +- DNS Lookup's own settings/behavior were **not changed** — only read as a reference pattern to copy. +- No new localization strings were added anywhere; every label reuses existing `Strings.*`/`StaticStrings.*` keys (chosen deliberately to avoid needing a Transifex sync for 17 languages). If that turns out to read awkwardly in the UI (e.g. the dialog's read-only "Name" field showing "DNS server(s)"), that's a place a dedicated new string could be added later. +- Global DNS servers remain **IP-only by design** (not a limitation to relax casually) — `DNSClient.Configure()` does a hard `IPAddress.Parse()`, so allowing hostnames there would require adding a resolution step first. diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index c7b9c5adaf..e344a05d89 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -72,6 +72,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 diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index ae78719a25..f2cce1b15f 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -374,6 +374,8 @@ public bool Network_UseCustomDNSServer } } + [Obsolete("Use Network_CustomDNSServers instead.")] + [field: Obsolete("Use Network_CustomDNSServers instead.")] public string Network_CustomDNSServer { get; @@ -387,6 +389,58 @@ public string Network_CustomDNSServer } } + public ObservableCollection 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; diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 5e293044fb..56eebfaa0e 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -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; @@ -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}\""); @@ -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 } diff --git a/Source/NETworkManager.Validators/MultipleIPAddressesValidator.cs b/Source/NETworkManager.Validators/MultipleIPAddressesValidator.cs deleted file mode 100644 index ed0d1a3d83..0000000000 --- a/Source/NETworkManager.Validators/MultipleIPAddressesValidator.cs +++ /dev/null @@ -1,27 +0,0 @@ -using NETworkManager.Localization.Resources; -using NETworkManager.Utilities; -using System.Globalization; -using System.Text.RegularExpressions; -using System.Windows.Controls; - -namespace NETworkManager.Validators; - -public class MultipleIPAddressesValidator : ValidationRule -{ - public override ValidationResult Validate(object value, CultureInfo cultureInfo) - { - if (value == null) - return ValidationResult.ValidResult; - - for (var index = 0; index < ((string)value).Split(';').Length; index++) - { - var ipAddress = ((string)value).Split(';')[index].Trim(); - - if (!RegexHelper.IPv4AddressRegex().IsMatch(ipAddress) && - !Regex.IsMatch(ipAddress.Trim(), RegexHelper.IPv6AddressRegex)) - return new ValidationResult(false, Strings.EnterOneOrMoreValidIPAddresses); - } - - return ValidationResult.ValidResult; - } -} From 3f425085ea726a3a8d12e3983ea7c64beb2cdd43 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:59:40 +0200 Subject: [PATCH 03/13] Feature: DNS server --- .../Resources/Strings.Designer.cs | 512 +++++++++--------- .../Resources/Strings.resx | 3 + .../ServerConnectionInfoProfileViewModel.cs | 16 +- .../ViewModels/SettingsNetworkViewModel.cs | 126 ++++- ...erverConnectionInfoProfileChildWindow.xaml | 1 + ...erConnectionInfoProfileChildWindow.xaml.cs | 7 +- .../Views/SettingsNetworkView.xaml | 68 ++- 7 files changed, 465 insertions(+), 268 deletions(-) diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 8df05e8a07..ccfb7aaabe 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -1,4 +1,4 @@ - //------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1898,34 +1898,7 @@ public static string CloseGroup { return ResourceManager.GetString("CloseGroup", resourceCulture); } } - - /// - /// Looks up a localized string similar to Start group. - /// - public static string StartGroup { - get { - return ResourceManager.GetString("StartGroup", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Group actions. - /// - public static string GroupActions { - get { - return ResourceManager.GetString("GroupActions", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Pause group. - /// - public static string PauseGroup { - get { - return ResourceManager.GetString("PauseGroup", resourceCulture); - } - } - + /// /// Looks up a localized string similar to Closing in {0} seconds.... /// @@ -2485,59 +2458,50 @@ public static string Crimson { } /// - /// Looks up a localized string similar to Ctrl+Alt+Del. - /// - public static string CtrlAltDel { - get { - return ResourceManager.GetString("CtrlAltDel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Task Manager (Ctrl+Shift+Esc). + /// Looks up a localized string similar to Imported from CSV file on {0}. /// - public static string TaskManager { + public static string Csv_ImportDescription { get { - return ResourceManager.GetString("TaskManager", resourceCulture); + return ResourceManager.GetString("Csv_ImportDescription", resourceCulture); } } - + /// - /// Looks up a localized string similar to Lock (Win+L). + /// Looks up a localized string similar to Expected CSV format — one profile per line:. /// - public static string Lock { + public static string CsvImportFormatHint { get { - return ResourceManager.GetString("Lock", resourceCulture); + return ResourceManager.GetString("CsvImportFormatHint", resourceCulture); } } - + /// - /// Looks up a localized string similar to Show Desktop (Win+D). + /// Looks up a localized string similar to A header row and the delimiter (semicolon, comma or tab) are detected automatically. The description column is optional. Entries without a host cannot be imported.. /// - public static string ShowDesktop { + public static string CsvImportFormatNote { get { - return ResourceManager.GetString("ShowDesktop", resourceCulture); + return ResourceManager.GetString("CsvImportFormatNote", resourceCulture); } } - + /// - /// Looks up a localized string similar to Explorer (Win+E). + /// Looks up a localized string similar to No entries were found in the CSV file.. /// - public static string Explorer { + public static string CsvNoEntriesFound { get { - return ResourceManager.GetString("Explorer", resourceCulture); + return ResourceManager.GetString("CsvNoEntriesFound", resourceCulture); } } - + /// - /// Looks up a localized string similar to Run dialog (Win+R). + /// Looks up a localized string similar to Ctrl+Alt+Del. /// - public static string RunDialog { + public static string CtrlAltDel { get { - return ResourceManager.GetString("RunDialog", resourceCulture); + return ResourceManager.GetString("CtrlAltDel", resourceCulture); } } - + /// /// Looks up a localized string similar to Currency. /// @@ -3045,43 +3009,7 @@ public static string Description { return ResourceManager.GetString("Description", resourceCulture); } } - - /// - /// Looks up a localized string similar to Placeholder. - /// - public static string Placeholder { - get { - return ResourceManager.GetString("Placeholder", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Path to the file or executable to run. The following placeholders can be used:. - /// - public static string HelpMessage_CustomCommandFilePath { - get { - return ResourceManager.GetString("HelpMessage_CustomCommandFilePath", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Arguments passed to the file or executable. The following placeholders can be used:. - /// - public static string HelpMessage_CustomCommandArguments { - get { - return ResourceManager.GetString("HelpMessage_CustomCommandArguments", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The following placeholder can be used:. - /// - public static string HelpMessage_ProfileHostPlaceholder { - get { - return ResourceManager.GetString("HelpMessage_ProfileHostPlaceholder", resourceCulture); - } - } - + /// /// Looks up a localized string similar to Deselect all. /// @@ -3459,7 +3387,7 @@ public static string DontFragment { return ResourceManager.GetString("DontFragment", resourceCulture); } } - + /// /// Looks up a localized string similar to Down. /// @@ -3468,7 +3396,7 @@ public static string Down { return ResourceManager.GetString("Down", resourceCulture); } } - + /// /// Looks up a localized string similar to Download. /// @@ -4336,7 +4264,7 @@ public static string ExpandHostView { return ResourceManager.GetString("ExpandHostView", resourceCulture); } } - + /// /// Looks up a localized string similar to Expand map view. /// @@ -4345,7 +4273,7 @@ public static string ExpandMapView { return ResourceManager.GetString("ExpandMapView", resourceCulture); } } - + /// /// Looks up a localized string similar to Experience. /// @@ -4373,6 +4301,15 @@ public static string ExperimentalFeatures { } } + /// + /// Looks up a localized string similar to Explorer (Win+E). + /// + public static string Explorer { + get { + return ResourceManager.GetString("Explorer", resourceCulture); + } + } + /// /// Looks up a localized string similar to Export. /// @@ -4813,7 +4750,7 @@ public static string FlushDNSCache { return ResourceManager.GetString("FlushDNSCache", resourceCulture); } } - + /// /// Looks up a localized string similar to DNS cache flushed. /// @@ -5012,6 +4949,15 @@ public static string Group { } } + /// + /// Looks up a localized string similar to Group actions. + /// + public static string GroupActions { + get { + return ResourceManager.GetString("GroupActions", resourceCulture); + } + } + /// /// Looks up a localized string similar to Group / domain. /// @@ -5145,6 +5091,24 @@ public static string HelpMessage_Credentials { } } + /// + /// Looks up a localized string similar to Arguments passed to the file or executable. The following placeholders can be used:. + /// + public static string HelpMessage_CustomCommandArguments { + get { + return ResourceManager.GetString("HelpMessage_CustomCommandArguments", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Path to the file or executable to run. The following placeholders can be used:. + /// + public static string HelpMessage_CustomCommandFilePath { + get { + return ResourceManager.GetString("HelpMessage_CustomCommandFilePath", resourceCulture); + } + } + /// /// Looks up a localized string similar to URL to a web service that can be reached via http or https and returns an IPv4 address e.g., "xx.xx.xx.xx" as response.. /// @@ -5235,6 +5199,15 @@ public static string HelpMessage_PasswordNotDisplayedCanBeOverwritten { } } + /// + /// Looks up a localized string similar to The following placeholder can be used:. + /// + public static string HelpMessage_ProfileHostPlaceholder { + get { + return ResourceManager.GetString("HelpMessage_ProfileHostPlaceholder", resourceCulture); + } + } + /// /// Looks up a localized string similar to Public IPv4 address reachable via ICMP.. /// @@ -5634,24 +5607,6 @@ public static string ImportProfiles_Method_ActiveDirectory { } } - /// - /// Looks up a localized string similar to Active Directory. - /// - public static string ImportProfiles_Source_ActiveDirectory { - get { - return ResourceManager.GetString("ImportProfiles_Source_ActiveDirectory", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CSV file. - /// - public static string ImportProfiles_Source_Csv { - get { - return ResourceManager.GetString("ImportProfiles_Source_Csv", resourceCulture); - } - } - /// /// Looks up a localized string similar to CSV file. /// @@ -5660,52 +5615,25 @@ public static string ImportProfiles_Method_Csv { return ResourceManager.GetString("ImportProfiles_Method_Csv", resourceCulture); } } - - /// - /// Looks up a localized string similar to Import profiles from CSV file. - /// - public static string ImportProfilesFromCsvFile { - get { - return ResourceManager.GetString("ImportProfilesFromCsvFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Imported from CSV file on {0}. - /// - public static string Csv_ImportDescription { - get { - return ResourceManager.GetString("Csv_ImportDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No entries were found in the CSV file.. - /// - public static string CsvNoEntriesFound { - get { - return ResourceManager.GetString("CsvNoEntriesFound", resourceCulture); - } - } - + /// - /// Looks up a localized string similar to Expected CSV format — one profile per line:. + /// Looks up a localized string similar to Active Directory. /// - public static string CsvImportFormatHint { + public static string ImportProfiles_Source_ActiveDirectory { get { - return ResourceManager.GetString("CsvImportFormatHint", resourceCulture); + return ResourceManager.GetString("ImportProfiles_Source_ActiveDirectory", resourceCulture); } } - + /// - /// Looks up a localized string similar to A header row and the delimiter (semicolon, comma or tab) are detected automatically. The description column is optional. Entries without a host cannot be imported.. + /// Looks up a localized string similar to CSV file. /// - public static string CsvImportFormatNote { + public static string ImportProfiles_Source_Csv { get { - return ResourceManager.GetString("CsvImportFormatNote", resourceCulture); + return ResourceManager.GetString("ImportProfiles_Source_Csv", resourceCulture); } } - + /// /// Looks up a localized string similar to Imported. /// @@ -5742,6 +5670,15 @@ public static string ImportProfilesDots { } } + /// + /// Looks up a localized string similar to Import profiles from CSV file. + /// + public static string ImportProfilesFromCsvFile { + get { + return ResourceManager.GetString("ImportProfilesFromCsvFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Import results. /// @@ -6012,7 +5949,7 @@ public static string IPv4Address { return ResourceManager.GetString("IPv4Address", resourceCulture); } } - + /// /// Looks up a localized string similar to IPv4 address added. /// @@ -6021,7 +5958,7 @@ public static string IPv4AddressAddedSuccessfully { return ResourceManager.GetString("IPv4AddressAddedSuccessfully", resourceCulture); } } - + /// /// Looks up a localized string similar to IPv4 address released and renewed. /// @@ -6030,7 +5967,7 @@ public static string IPv4AddressReleasedAndRenewedSuccessfully { return ResourceManager.GetString("IPv4AddressReleasedAndRenewedSuccessfully", resourceCulture); } } - + /// /// Looks up a localized string similar to IPv4 address released. /// @@ -6039,49 +5976,22 @@ public static string IPv4AddressReleasedSuccessfully { return ResourceManager.GetString("IPv4AddressReleasedSuccessfully", resourceCulture); } } - - /// - /// Looks up a localized string similar to IPv4 address renewed. - /// - public static string IPv4AddressRenewedSuccessfully { - get { - return ResourceManager.GetString("IPv4AddressRenewedSuccessfully", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to IPv6 address released and renewed. - /// - public static string IPv6AddressReleasedAndRenewedSuccessfully { - get { - return ResourceManager.GetString("IPv6AddressReleasedAndRenewedSuccessfully", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to IPv6 address released. - /// - public static string IPv6AddressReleasedSuccessfully { - get { - return ResourceManager.GetString("IPv6AddressReleasedSuccessfully", resourceCulture); - } - } - + /// - /// Looks up a localized string similar to IPv6 address renewed. + /// Looks up a localized string similar to IPv4 address removed. /// - public static string IPv6AddressRenewedSuccessfully { + public static string IPv4AddressRemovedSuccessfully { get { - return ResourceManager.GetString("IPv6AddressRenewedSuccessfully", resourceCulture); + return ResourceManager.GetString("IPv4AddressRemovedSuccessfully", resourceCulture); } } - + /// - /// Looks up a localized string similar to IPv4 address removed. + /// Looks up a localized string similar to IPv4 address renewed. /// - public static string IPv4AddressRemovedSuccessfully { + public static string IPv4AddressRenewedSuccessfully { get { - return ResourceManager.GetString("IPv4AddressRemovedSuccessfully", resourceCulture); + return ResourceManager.GetString("IPv4AddressRenewedSuccessfully", resourceCulture); } } @@ -6130,6 +6040,33 @@ public static string IPv6AddressLinkLocal { } } + /// + /// Looks up a localized string similar to IPv6 address released and renewed. + /// + public static string IPv6AddressReleasedAndRenewedSuccessfully { + get { + return ResourceManager.GetString("IPv6AddressReleasedAndRenewedSuccessfully", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv6 address released. + /// + public static string IPv6AddressReleasedSuccessfully { + get { + return ResourceManager.GetString("IPv6AddressReleasedSuccessfully", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IPv6 address renewed. + /// + public static string IPv6AddressRenewedSuccessfully { + get { + return ResourceManager.GetString("IPv6AddressRenewedSuccessfully", resourceCulture); + } + } + /// /// Looks up a localized string similar to IPv6-Default-Gateway. /// @@ -6688,6 +6625,15 @@ public static string LocationOfTheImport { } } + /// + /// Looks up a localized string similar to Lock (Win+L). + /// + public static string Lock { + get { + return ResourceManager.GetString("Lock", resourceCulture); + } + } + /// /// Looks up a localized string similar to Log. /// @@ -6805,6 +6751,15 @@ public static string Management { } } + /// + /// Looks up a localized string similar to Map. + /// + public static string Map { + get { + return ResourceManager.GetString("Map", resourceCulture); + } + } + /// /// Looks up a localized string similar to Master Password. /// @@ -6840,34 +6795,7 @@ public static string MaxHostThreads { return ResourceManager.GetString("MaxHostThreads", resourceCulture); } } - - /// - /// Looks up a localized string similar to Map. - /// - public static string Map { - get { - return ResourceManager.GetString("Map", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Scroll = Zoom, Drag = Pan. - /// - public static string ScrollToZoomDragToPan { - get { - return ResourceManager.GetString("ScrollToZoomDragToPan", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unknown. - /// - public static string Unknown { - get { - return ResourceManager.GetString("Unknown", resourceCulture); - } - } - + /// /// Looks up a localized string similar to Maximum. /// @@ -6912,6 +6840,7 @@ public static string MaxPortThreads { return ResourceManager.GetString("MaxPortThreads", resourceCulture); } } + /// /// Looks up a localized string similar to Measured time. /// @@ -7298,7 +7227,7 @@ public static string NetworkInterface { return ResourceManager.GetString("NetworkInterface", resourceCulture); } } - + /// /// Looks up a localized string similar to Configuration applied. /// @@ -7307,7 +7236,7 @@ public static string NetworkInterfaceConfigurationAppliedSuccessfully { return ResourceManager.GetString("NetworkInterfaceConfigurationAppliedSuccessfully", resourceCulture); } } - + /// /// Looks up a localized string similar to Configuring the network interface requires elevated rights!. /// @@ -7316,7 +7245,7 @@ public static string NetworkInterfaceConfigureAdminMessage { return ResourceManager.GetString("NetworkInterfaceConfigureAdminMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Network kind. /// @@ -7451,7 +7380,7 @@ public static string NewTab { return ResourceManager.GetString("NewTab", resourceCulture); } } - + /// /// Looks up a localized string similar to Next. /// @@ -7460,7 +7389,7 @@ public static string Next { return ResourceManager.GetString("Next", resourceCulture); } } - + /// /// Looks up a localized string similar to No. /// @@ -7642,6 +7571,15 @@ public static string NotificationSuccessThreshold { } } + /// + /// Looks up a localized string similar to Not set. + /// + public static string NotSet { + get { + return ResourceManager.GetString("NotSet", resourceCulture); + } + } + /// /// Looks up a localized string similar to No update available!. /// @@ -7823,23 +7761,23 @@ public static string On { } /// - /// Looks up a localized string similar to Only numbers can be entered!. + /// Looks up a localized string similar to Only CSV files are allowed!. /// - public static string OnlyNumbersCanBeEntered { + public static string OnlyCsvFilesAllowed { get { - return ResourceManager.GetString("OnlyNumbersCanBeEntered", resourceCulture); + return ResourceManager.GetString("OnlyCsvFilesAllowed", resourceCulture); } } /// - /// Looks up a localized string similar to Only CSV files are allowed!. + /// Looks up a localized string similar to Only numbers can be entered!. /// - public static string OnlyCsvFilesAllowed { + public static string OnlyNumbersCanBeEntered { get { - return ResourceManager.GetString("OnlyCsvFilesAllowed", resourceCulture); + return ResourceManager.GetString("OnlyNumbersCanBeEntered", resourceCulture); } } - + /// /// Looks up a localized string similar to Only when using the full screen. /// @@ -8065,6 +8003,15 @@ public static string Parameter { } } + /// + /// Looks up a localized string similar to Parse. + /// + public static string Parse { + get { + return ResourceManager.GetString("Parse", resourceCulture); + } + } + /// /// Looks up a localized string similar to Password. /// @@ -8083,15 +8030,6 @@ public static string PasswordsDoNotMatch { } } - /// - /// Looks up a localized string similar to Parse. - /// - public static string Parse { - get { - return ResourceManager.GetString("Parse", resourceCulture); - } - } - /// /// Looks up a localized string similar to Paste. /// @@ -8127,7 +8065,7 @@ public static string Pause { return ResourceManager.GetString("Pause", resourceCulture); } } - + /// /// Looks up a localized string similar to Paused. /// @@ -8136,7 +8074,16 @@ public static string Paused { return ResourceManager.GetString("Paused", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Pause group. + /// + public static string PauseGroup { + get { + return ResourceManager.GetString("PauseGroup", resourceCulture); + } + } + /// /// Looks up a localized string similar to Performance. /// @@ -8245,6 +8192,15 @@ public static string Pink { } } + /// + /// Looks up a localized string similar to Placeholder. + /// + public static string Placeholder { + get { + return ResourceManager.GetString("Placeholder", resourceCulture); + } + } + /// /// Looks up a localized string similar to Play sound on status change. /// @@ -8253,7 +8209,7 @@ public static string PlaySoundOnStatusChange { return ResourceManager.GetString("PlaySoundOnStatusChange", resourceCulture); } } - + /// /// Looks up a localized string similar to (+{0} more). /// @@ -8262,7 +8218,7 @@ public static string PlusXMore { return ResourceManager.GetString("PlusXMore", resourceCulture); } } - + /// /// Looks up a localized string similar to Port. /// @@ -10172,7 +10128,7 @@ public static string Resource_ListTLD_Description { return ResourceManager.GetString("Resource_ListTLD_Description", resourceCulture); } } - + /// /// Looks up a localized string similar to GeoJSON conversion of Natural Earth vector map data (CC0-1.0).. /// @@ -10181,7 +10137,7 @@ public static string Resource_NaturalEarth_Description { return ResourceManager.GetString("Resource_NaturalEarth_Description", resourceCulture); } } - + /// /// Looks up a localized string similar to OUI data from ieee.org.. /// @@ -10447,6 +10403,15 @@ public static string RunCommandDotsWithHotKey { } } + /// + /// Looks up a localized string similar to Run dialog (Win+R). + /// + public static string RunDialog { + get { + return ResourceManager.GetString("RunDialog", resourceCulture); + } + } + /// /// Looks up a localized string similar to Run speed test. /// @@ -10519,6 +10484,15 @@ public static string ScanPorts { } } + /// + /// Looks up a localized string similar to Scroll = Zoom, Drag = Pan. + /// + public static string ScrollToZoomDragToPan { + get { + return ResourceManager.GetString("ScrollToZoomDragToPan", resourceCulture); + } + } + /// /// Looks up a localized string similar to Search. /// @@ -10973,6 +10947,15 @@ public static string ShowCurrentApplicationTitle { } } + /// + /// Looks up a localized string similar to Show Desktop (Win+D). + /// + public static string ShowDesktop { + get { + return ResourceManager.GetString("ShowDesktop", resourceCulture); + } + } + /// /// Looks up a localized string similar to Show error message. /// @@ -10990,7 +10973,7 @@ public static string ShowLocalLicenses { return ResourceManager.GetString("ShowLocalLicenses", resourceCulture); } } - + /// /// Looks up a localized string similar to Show map. /// @@ -10999,7 +10982,7 @@ public static string ShowMap { return ResourceManager.GetString("ShowMap", resourceCulture); } } - + /// /// Looks up a localized string similar to Show notification popup on status change. /// @@ -11469,6 +11452,15 @@ public static string StarForkProjectOnGitHub { } } + /// + /// Looks up a localized string similar to Start group. + /// + public static string StartGroup { + get { + return ResourceManager.GetString("StartGroup", resourceCulture); + } + } + /// /// Looks up a localized string similar to Start minimized in tray. /// @@ -11676,6 +11668,15 @@ public static string Tags { } } + /// + /// Looks up a localized string similar to Task Manager (Ctrl+Shift+Esc). + /// + public static string TaskManager { + get { + return ResourceManager.GetString("TaskManager", resourceCulture); + } + } + /// /// Looks up a localized string similar to Taupe. /// @@ -12234,6 +12235,15 @@ public static string Unit { } } + /// + /// Looks up a localized string similar to Unknown. + /// + public static string Unknown { + get { + return ResourceManager.GetString("Unknown", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unkown error!. /// @@ -12278,7 +12288,7 @@ public static string UntrayBringWindowToForeground { return ResourceManager.GetString("UntrayBringWindowToForeground", resourceCulture); } } - + /// /// Looks up a localized string similar to Up. /// @@ -12287,7 +12297,7 @@ public static string Up { return ResourceManager.GetString("Up", resourceCulture); } } - + /// /// Looks up a localized string similar to Update. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 5533818793..bdf839e1c5 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -4496,4 +4496,7 @@ Cloudflare may log your IP address and network information. See Cloudflare's pri Stop + + Not set + \ No newline at end of file diff --git a/Source/NETworkManager/ViewModels/ServerConnectionInfoProfileViewModel.cs b/Source/NETworkManager/ViewModels/ServerConnectionInfoProfileViewModel.cs index b0def83ee4..b654fa55a3 100644 --- a/Source/NETworkManager/ViewModels/ServerConnectionInfoProfileViewModel.cs +++ b/Source/NETworkManager/ViewModels/ServerConnectionInfoProfileViewModel.cs @@ -16,13 +16,14 @@ public class ServerConnectionInfoProfileViewModel : ViewModelBase public ServerConnectionInfoProfileViewModel(Action saveCommand, Action cancelHandler, (List UsedNames, bool IsEdited, bool allowOnlyIPAddress) options, ServerConnectionInfo defaultValues, - ServerConnectionInfoProfile info = null) + ServerConnectionInfoProfile info = null, bool isNameReadOnly = false) { SaveCommand = new RelayCommand(_ => saveCommand(this)); CancelCommand = new RelayCommand(_ => cancelHandler(this)); UsedNames = options.UsedNames; AllowOnlyIPAddress = options.allowOnlyIPAddress; + IsNameReadOnly = isNameReadOnly; _defaultValues = defaultValues; @@ -100,6 +101,19 @@ public bool AllowOnlyIPAddress } } + public bool IsNameReadOnly + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + #endregion public string Name diff --git a/Source/NETworkManager/ViewModels/SettingsNetworkViewModel.cs b/Source/NETworkManager/ViewModels/SettingsNetworkViewModel.cs index 3ea8b6ce19..304511bfa7 100644 --- a/Source/NETworkManager/ViewModels/SettingsNetworkViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsNetworkViewModel.cs @@ -1,4 +1,12 @@ +using MahApps.Metro.SimpleChildWindow; +using NETworkManager.Localization.Resources; +using NETworkManager.Models.Network; using NETworkManager.Settings; +using NETworkManager.Utilities; +using NETworkManager.Views; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; namespace NETworkManager.ViewModels; @@ -8,6 +16,11 @@ public class SettingsNetworkViewModel : ViewModelBase private readonly bool _isLoading; + /// + /// Default values for the DNS server profile dialog. + /// + private readonly ServerConnectionInfo _profileDialogDefaultValues = new("1.1.1.1", 53, TransportProtocol.Udp); + public bool UseCustomDNSServer { get; @@ -24,8 +37,52 @@ public bool UseCustomDNSServer } } + public string CustomDNSServersDisplay + { + get; + private set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + + public bool AddDNSSuffix + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.Network_AddDNSSuffix = value; + + field = value; + OnPropertyChanged(); + } + } + + public bool UseCustomDNSSuffix + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.Network_UseCustomDNSSuffix = value; - public string CustomDNSServer + field = value; + OnPropertyChanged(); + } + } + + public string CustomDNSSuffix { get; set @@ -34,7 +91,7 @@ public string CustomDNSServer return; if (!_isLoading) - SettingsManager.Current.Network_CustomDNSServer = value.Replace(" ", ""); + SettingsManager.Current.Network_CustomDNSSuffix = value; field = value; OnPropertyChanged(); @@ -87,8 +144,11 @@ private void LoadSettings() { UseCustomDNSServer = SettingsManager.Current.Network_UseCustomDNSServer; - if (SettingsManager.Current.Network_CustomDNSServer != null) - CustomDNSServer = string.Join("; ", SettingsManager.Current.Network_CustomDNSServer); + RefreshCustomDNSServersDisplay(); + + AddDNSSuffix = SettingsManager.Current.Network_AddDNSSuffix; + UseCustomDNSSuffix = SettingsManager.Current.Network_UseCustomDNSSuffix; + CustomDNSSuffix = SettingsManager.Current.Network_CustomDNSSuffix; if (SettingsManager.Current.Network_ResolveHostnamePreferIPv4) ResolveHostnamePreferIPv4 = true; @@ -96,5 +156,61 @@ private void LoadSettings() ResolveHostnamePreferIPv6 = true; } + private void RefreshCustomDNSServersDisplay() + { + CustomDNSServersDisplay = SettingsManager.Current.Network_CustomDNSServers.Count == 0 ? Strings.NotSet : string.Join("; ", SettingsManager.Current.Network_CustomDNSServers); + } + + #endregion + + #region Commands + + public ICommand EditCustomDNSServersCommand => new RelayCommand(_ => EditCustomDNSServersAction()); + + private void EditCustomDNSServersAction() + { + _ = EditCustomDNSServers(); + } + + #endregion + + #region Methods + + /// + /// Opens the DNS server profile dialog to edit the global custom DNS servers. The profile name + /// is fixed and read-only because there is only a single global list, not multiple named profiles. + /// + private async Task EditCustomDNSServers() + { + var childWindow = new ServerConnectionInfoProfileChildWindow(); + + var info = new ServerConnectionInfoProfile(Strings.Default, + [.. SettingsManager.Current.Network_CustomDNSServers]); + + var childWindowViewModel = new ServerConnectionInfoProfileViewModel(instance => + { + childWindow.IsOpen = false; + ConfigurationManager.Current.IsChildWindowOpen = false; + + SettingsManager.Current.Network_CustomDNSServers = new(instance.Servers); + + RefreshCustomDNSServersDisplay(); + }, _ => + { + childWindow.IsOpen = false; + ConfigurationManager.Current.IsChildWindowOpen = false; + }, + ([], true, true), + _profileDialogDefaultValues, info, true); + + childWindow.Title = Strings.EditDNSServer; + + childWindow.DataContext = childWindowViewModel; + + ConfigurationManager.Current.IsChildWindowOpen = true; + + await Application.Current.MainWindow.ShowChildWindowAsync(childWindow); + } + #endregion -} \ No newline at end of file +} diff --git a/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml b/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml index 8e2cadf357..32723cdc14 100644 --- a/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml +++ b/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml @@ -43,6 +43,7 @@ diff --git a/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml.cs b/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml.cs index cc61fbd0b5..225345fe59 100644 --- a/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml.cs +++ b/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml.cs @@ -15,9 +15,14 @@ public ServerConnectionInfoProfileChildWindow() private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e) { + var isNameReadOnly = (DataContext as ServerConnectionInfoProfileViewModel)?.IsNameReadOnly ?? false; + Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate { - TextBoxName.Focus(); + if (isNameReadOnly) + TextBoxServer.Focus(); + else + TextBoxName.Focus(); })); } diff --git a/Source/NETworkManager/Views/SettingsNetworkView.xaml b/Source/NETworkManager/Views/SettingsNetworkView.xaml index 52dd23a682..8ac7945595 100644 --- a/Source/NETworkManager/Views/SettingsNetworkView.xaml +++ b/Source/NETworkManager/Views/SettingsNetworkView.xaml @@ -1,4 +1,4 @@ - - + - + - + + + + + + + + - + - + @@ -35,4 +83,4 @@ --> - \ No newline at end of file + From a68faba8e6e55a65190e1820a499544ddccde471 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:50:06 +0200 Subject: [PATCH 04/13] Feature: DNS settings --- .../Network/DNSLookup.cs | 20 ++++--- .../GlobalStaticConfiguration.cs | 3 + Source/NETworkManager.Utilities/DNSClient.cs | 24 ++++++++ .../DNSClientSettings.cs | 10 ++++ Source/NETworkManager/MainWindow.xaml.cs | 57 +++++++++++++++---- 5 files changed, 95 insertions(+), 19 deletions(-) diff --git a/Source/NETworkManager.Models/Network/DNSLookup.cs b/Source/NETworkManager.Models/Network/DNSLookup.cs index da84a900d1..b1162a18cc 100644 --- a/Source/NETworkManager.Models/Network/DNSLookup.cs +++ b/Source/NETworkManager.Models/Network/DNSLookup.cs @@ -108,13 +108,15 @@ private void OnLookupComplete() /// List of DNS servers as . private IEnumerable GetDnsServer(IEnumerable dnsServers = null) { - List servers = []; + // Use Windows dns servers + List 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; } @@ -124,9 +126,9 @@ private IEnumerable GetDnsServer(IEnumerable d /// /// List of hosts /// List of host with DNS suffix - private IEnumerable GetHostWithSuffix(IEnumerable hosts) + private IEnumerable GetHostsWithSuffix(IEnumerable hosts) { - return hosts.Select(host => host.Contains('.') ? host : $"{host}.{_suffix}").ToList(); + return [.. hosts.Select(host => host.Contains('.') ? host : $"{host}.{_suffix}")]; } /// @@ -138,7 +140,7 @@ public void ResolveAsync(IEnumerable 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 => diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index e344a05d89..24637a072d 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -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. diff --git a/Source/NETworkManager.Utilities/DNSClient.cs b/Source/NETworkManager.Utilities/DNSClient.cs index 91156be90d..8a32bcb19b 100644 --- a/Source/NETworkManager.Utilities/DNSClient.cs +++ b/Source/NETworkManager.Utilities/DNSClient.cs @@ -32,6 +32,11 @@ public class DNSClient : SingletonBase /// private DNSClientSettings _settings; + /// + /// Indicates if the DNS suffix should be added to a hostname without a dot before resolving it. + /// + private bool _addSuffix; + /// /// Method to configure the DNS client. /// @@ -40,6 +45,8 @@ public void Configure(DNSClientSettings settings) { _settings = settings; + _addSuffix = _settings.AddDNSSuffix && !string.IsNullOrEmpty(_settings.DNSSuffix); + Log.Debug("Configure - Configuring DNS client..."); if (_settings.UseCustomDNSServers) @@ -88,6 +95,8 @@ public async Task ResolveAAsync(string query) if (!_isConfigured) throw new DNSClientNotConfiguredException(NotConfiguredMessage); + query = AddDNSSuffixIfConfigured(query); + try { var result = await _client.QueryAsync(query, QueryType.A); @@ -131,6 +140,8 @@ public async Task ResolveAaaaAsync(string query) if (!_isConfigured) throw new DNSClientNotConfiguredException(NotConfiguredMessage); + query = AddDNSSuffixIfConfigured(query); + try { var result = await _client.QueryAsync(query, QueryType.AAAA); @@ -255,6 +266,19 @@ public async Task ResolvePtrAsync(IPAddress ipAddress) } } + /// + /// Appends the configured DNS suffix to a hostname without a dot (forward lookups only). + /// FQDNs (containing a dot) are returned unchanged. + /// + /// Hostname or FQDN as string like "example.com". + /// Query with the DNS suffix appended, if configured and applicable. + private string AddDNSSuffixIfConfigured(string query) + { + return _addSuffix && !string.IsNullOrEmpty(query) && !query.Contains('.') + ? $"{query}.{_settings.DNSSuffix}" + : query; + } + /// /// 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 diff --git a/Source/NETworkManager.Utilities/DNSClientSettings.cs b/Source/NETworkManager.Utilities/DNSClientSettings.cs index d08a8191e3..ebac3a93aa 100644 --- a/Source/NETworkManager.Utilities/DNSClientSettings.cs +++ b/Source/NETworkManager.Utilities/DNSClientSettings.cs @@ -23,4 +23,14 @@ public DNSClientSettings() /// List of name servers as Tuple (string Server, int Port). /// public IEnumerable<(string Server, int Port)> DNSServers { get; set; } + + /// + /// Add the DNS suffix to a hostname without a dot before resolving it (A/AAAA queries only). + /// + public bool AddDNSSuffix { get; set; } + + /// + /// DNS suffix to append. Either the custom DNS suffix from the settings or the Windows DNS suffix. + /// + public string DNSSuffix { get; set; } } \ No newline at end of file diff --git a/Source/NETworkManager/MainWindow.xaml.cs b/Source/NETworkManager/MainWindow.xaml.cs index 264269aec4..095d926df2 100644 --- a/Source/NETworkManager/MainWindow.xaml.cs +++ b/Source/NETworkManager/MainWindow.xaml.cs @@ -64,14 +64,32 @@ private void SettingsManager_PropertyChanged(object sender, PropertyChangedEvent break; // Update DNS server if changed in the settings + // Delayed via a timer, so rapid/successive changes (e.g. typing in the custom DNS + // suffix textbox) don't reconfigure the DNS client on every single change. case nameof(SettingsInfo.Network_UseCustomDNSServer): - case nameof(SettingsInfo.Network_CustomDNSServer): - ConfigureDNSServer(); + case nameof(SettingsInfo.Network_CustomDNSServers): + case nameof(SettingsInfo.Network_AddDNSSuffix): + case nameof(SettingsInfo.Network_UseCustomDNSSuffix): + case nameof(SettingsInfo.Network_CustomDNSSuffix): + // Stop + Start (instead of just Start) to reset the due time on every change -- + // DispatcherTimer.Start() is a no-op while it's already running. + _configureDNSServerDispatcherTimer.Stop(); + _configureDNSServerDispatcherTimer.Start(); break; } } + /// + /// Apply pending DNS server settings changes once they stopped changing for a short delay. + /// + private void ConfigureDNSServerDispatcherTimer_Tick(object sender, EventArgs e) + { + _configureDNSServerDispatcherTimer.Stop(); + + ConfigureDNSServer(); + } + #endregion #region Bugfixes @@ -101,6 +119,12 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = null) private NotifyIcon _notifyIcon; private StatusWindow _statusWindow; + /// + /// Timer to delay while the related settings are still changing + /// (e.g. while typing in the custom DNS suffix textbox), so it's only applied once. + /// + private readonly DispatcherTimer _configureDNSServerDispatcherTimer = new(); + private readonly bool _isLoading; private bool _isProfileFilesLoading; private bool _isProfileFileUpdating; @@ -404,6 +428,10 @@ public MainWindow() // Load and change appearance AppearanceManager.Load(); + // Delay applying DNS server settings change + _configureDNSServerDispatcherTimer.Interval = GlobalStaticConfiguration.NetworkConfigApplyDispatcherTimerTimeSpan; + _configureDNSServerDispatcherTimer.Tick += ConfigureDNSServerDispatcherTimer_Tick; + // Load and configure DNS server ConfigureDNSServer(); @@ -1866,21 +1894,22 @@ private void ConfigureDNSServer() if (SettingsManager.Current.Network_UseCustomDNSServer) { - if (!string.IsNullOrEmpty(SettingsManager.Current.Network_CustomDNSServer)) + if (SettingsManager.Current.Network_CustomDNSServers.Count > 0) { - Log.Info($"Use custom DNS servers ({SettingsManager.Current.Network_CustomDNSServer})..."); - - List<(string Server, int Port)> dnsServers = SettingsManager.Current.Network_CustomDNSServer.Split(";") - .Select(dnsServer => (dnsServer, 53)) - .ToList(); + Log.Info( + $"Use custom DNS servers ({string.Join("; ", SettingsManager.Current.Network_CustomDNSServers)})..."); dnsSettings.UseCustomDNSServers = true; - dnsSettings.DNSServers = dnsServers; + dnsSettings.DNSServers = + [ + .. SettingsManager.Current.Network_CustomDNSServers + .Select(dnsServer => (dnsServer.Server, dnsServer.Port)) + ]; } else { Log.Info( - $"Custom DNS servers could not be set (Setting \"{nameof(SettingsManager.Current.Network_CustomDNSServer)}\" has value \"{SettingsManager.Current.Network_CustomDNSServer}\")! Fallback to Windows default DNS servers..."); + $"Custom DNS servers could not be set (Setting \"{nameof(SettingsManager.Current.Network_CustomDNSServers)}\" is empty)! Fallback to Windows default DNS servers..."); } } else @@ -1888,6 +1917,14 @@ private void ConfigureDNSServer() Log.Info("Use Windows default DNS servers..."); } + if (SettingsManager.Current.Network_AddDNSSuffix) + { + dnsSettings.AddDNSSuffix = true; + dnsSettings.DNSSuffix = SettingsManager.Current.Network_UseCustomDNSSuffix + ? SettingsManager.Current.Network_CustomDNSSuffix?.TrimStart('.') + : IPGlobalProperties.GetIPGlobalProperties().DomainName; + } + DNSClient.GetInstance().Configure(dnsSettings); } From 0f62d5212579d9f3c581522e14ec128931a9bba0 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:51:07 +0200 Subject: [PATCH 05/13] Delete HANDOVER.md --- HANDOVER.md | 70 ----------------------------------------------------- 1 file changed, 70 deletions(-) delete mode 100644 HANDOVER.md diff --git a/HANDOVER.md b/HANDOVER.md deleted file mode 100644 index 2b18b8d5d7..0000000000 --- a/HANDOVER.md +++ /dev/null @@ -1,70 +0,0 @@ -# Handover: Global DNS suffix + custom DNS server rework - -**Status:** Implemented, uncommitted, NOT built/tested (no Windows/.NET SDK in the sandbox that did this work). -**Branch:** `feature/3559` (clean branch off `main` @ `38989cb22`, no commits yet — all changes below are working-tree edits only). -**This file is untracked** (not added to git) — delete it once you're done, or `git add`/commit it if you want it kept. - -## What was asked - -Extend the global Network settings (`SettingsNetworkViewModel`/`SettingsNetworkView`) to add two things that already exist in the DNS Lookup tool's own settings, making them apply to *all* tools that resolve hostnames (Ping, Traceroute, PortScanner, IPScanner, Connection, NTP Lookup, etc.), not just DNS Lookup: - -1. **DNS suffix support** — "Add DNS suffix (primary) to hostname" + "Use custom DNS suffix" + the suffix textbox. -2. **Rework of the custom DNS server input** — was a single semicolon-separated IP-only textbox; wanted something closer to DNS Lookup's server dialog (label + Edit button instead of a raw textbox). - -## Research findings (still true, useful context) - -- There are **two independent DNS resolution paths** in the codebase: - - The shared singleton `NETworkManager.Utilities.DNSClient`, configured once from `MainWindow.ConfigureDNSServer()` off the `Network_*` settings. This is the resolver behind PTR (reverse) lookups in Ping/Traceroute/PortScanner/IPScanner/Connection/NetworkConnectionWidget, **and** the forward A/AAAA chokepoint `DNSClientHelper.ResolveAorAaaaAsync` used by `HostRangeHelper` (host-range parsing shared by Ping/Traceroute/PortScanner/IPScanner) and `SNTPLookup`. This is the one place that needed suffix support to make it "global." - - DNS Lookup's own independent path in `NETworkManager.Models.Network.DNSLookup.cs`, which builds a fresh `LookupClient` per query and never touches the shared singleton. **Not touched by this change** — it already had its own suffix logic (`DNSLookup_AddDNSSuffix`/`DNSLookup_UseCustomDNSSuffix`/`DNSLookup_CustomDNSSuffix`), which was the template copied for the global settings. -- DNS Lookup's server-editing dialog (`ServerConnectionInfoProfileChildWindow` + `ServerConnectionInfoProfileViewModel`) is a **named-profile** editor (Name + list of Server:Port). Global settings only need one unnamed list, so it's reused with a new `isNameReadOnly` flag rather than building a new dialog. -- `DNSClient.Configure()` calls `IPAddress.Parse(server)` directly on custom DNS server entries — **custom DNS servers must stay IP-only** (hostnames would throw). DNS Lookup's dialog allows hostnames for its own servers; ours is opened with `allowOnlyIPAddress: true`. - -## Decisions made (confirmed with user via AskUserQuestion) - -1. **"Add DNS suffix" defaults to ON** (matches DNS Lookup's own default), even though this changes hostname-resolution behavior for existing users across Ping/Traceroute/PortScanner/IPScanner/SNTP after upgrade. -2. **Custom DNS server editing reuses the DNS Lookup profile dialog** (`ServerConnectionInfoProfileChildWindow`/`ServerConnectionInfoProfileViewModel`), opened with the profile name fixed to `Strings.DNSServers` ("DNS server(s)") and **read-only** (new `IsNameReadOnly` property/binding), since there's only one global list, not multiple named profiles. -3. **Per-server port is configurable** (previously hardcoded to 53). DnsClient.net already supports arbitrary `IPEndPoint` per server, so this was low cost. - -## Files changed (all uncommitted) - -| File | Change | -|---|---| -| `Source/NETworkManager.Settings/GlobalStaticConfiguration.cs` | Added `Network_AddDNSSuffix => true` default. **Note:** a linter/format pass (not me) reordered this line to sit alphabetically above `Network_ResolveHostnamePreferIPv4` — that reorder is intentional, don't revert it. | -| `Source/NETworkManager.Settings/SettingsInfo.cs` | Added `Network_CustomDNSServers` (`ObservableCollection`), `Network_AddDNSSuffix`, `Network_UseCustomDNSSuffix`, `Network_CustomDNSSuffix`. Marked old `Network_CustomDNSServer` (string) `[Obsolete]` — kept only for the migration below. | -| `Source/NETworkManager.Settings/SettingsManager.cs` | New upgrade step `UpgradeTo_2026_8_17_0()` (registered in the `Upgrade()` dispatcher chain) migrates the old semicolon-separated `Network_CustomDNSServer` string into `Network_CustomDNSServers` at port 53/UDP, preserving current behavior for upgraders. **Version number `2026.8.17.0` was a placeholder guess (today's date) — confirm/adjust to match whatever version this actually ships in**, following the project's `yyyy.M.d.0` date-based versioning (see `AGENTS.md`). | -| `Source/NETworkManager.Utilities/DNSClientSettings.cs` | Added `AddDNSSuffix` (bool) and `DNSSuffix` (string, already-resolved value) fields. | -| `Source/NETworkManager.Utilities/DNSClient.cs` | `Configure()` precomputes `_addSuffix`. New private `AddDNSSuffixIfConfigured(query)` appends `.{suffix}` to hostnames without a dot; called from `ResolveAAsync`/`ResolveAaaaAsync` only (not PTR/CNAME — matches `DNSLookup.cs`'s behavior of skipping suffix for reverse lookups). | -| `Source/NETworkManager/MainWindow.xaml.cs` | `ConfigureDNSServer()` rewritten to build the server list from `Network_CustomDNSServers` (real per-entry ports) and to populate `AddDNSSuffix`/`DNSSuffix` on `DNSClientSettings` (custom suffix trimmed of leading `.`, else `IPGlobalProperties.GetIPGlobalProperties().DomainName`, mirroring `DNSLookupViewModel.QueryAsync()`). `SettingsManager_PropertyChanged` switch extended with cases for `Network_CustomDNSServers`, `Network_AddDNSSuffix`, `Network_UseCustomDNSSuffix`, `Network_CustomDNSSuffix` so live settings changes reconfigure DNS immediately (same as the pre-existing two cases). | -| `Source/NETworkManager/ViewModels/ServerConnectionInfoProfileViewModel.cs` | New optional ctor param `isNameReadOnly = false` → new bindable `IsNameReadOnly` property. | -| `Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml` | `TextBoxName.IsReadOnly` bound to `IsNameReadOnly`. | -| `Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml.cs` | `ChildWindow_OnLoaded` now focuses `TextBoxServer` instead of `TextBoxName` when the name is read-only (small UX polish so tab focus doesn't land on an uneditable field). | -| `Source/NETworkManager/ViewModels/SettingsNetworkViewModel.cs` | Rewritten. New: `CustomDNSServersDisplay` (read-only joined-string of `Network_CustomDNSServers`), `AddDNSSuffix`/`UseCustomDNSSuffix`/`CustomDNSSuffix` properties (copied pattern from `DNSLookupSettingsViewModel`), `EditCustomDNSServersCommand` → `EditCustomDNSServers()` which opens the reused profile dialog (see decisions above) and writes the result back to `Network_CustomDNSServers`. Old `CustomDNSServer` string property removed entirely. | -| `Source/NETworkManager/Views/SettingsNetworkView.xaml` | Old DNS server `TextBox` replaced with a label (`CustomDNSServersDisplay`) + Edit icon button (`EditCustomDNSServersCommand`, `iconPacks:Modern Kind=Edit`, tooltip `Strings.EditDNSServer`). Added suffix toggle/textbox section copied verbatim in structure from `DNSLookupSettingsView.xaml` (reuses existing localized strings — no new resx keys needed anywhere in this change). | -| `Source/NETworkManager.Validators/MultipleIPAddressesValidator.cs` | **Deleted** — was only referenced by the old DNS server textbox, now dead code. Confirmed via repo-wide grep before deleting. | -| `Website/docs/settings/network.md` | Updated to document the new "DNS server(s)" editing UX (now IP+port, edited via dialog) and the three new suffix settings. | - -## Verification status — IMPORTANT, not done yet - -**Nothing has been compiled or run.** This sandbox has no `dotnet` SDK and the project targets `net10.0-windows10.0.22621.0` (WPF, Windows-only), so it can't be built here at all. Verification so far was manual code tracing only: -- Confirmed no other files reference the deleted validator or the removed `CustomDNSServer` VM property (repo-wide grep). -- Confirmed `ServerConnectionInfo`/`TransportProtocol`/`ObservableCollection` patterns already exist and serialize fine elsewhere (`DNSLookup_DNSServers` uses the same shapes) — System.Text.Json settings persistence should just work. -- Confirmed `ServerValidator` genuinely enforces IP-only when `AllowOnlyIPAddress=true` (checked the validator source). -- Confirmed `DNSClientSettings` is only constructed in one place (`MainWindow.ConfigureDNSServer()`), so no other caller needed updating. - -**Next session should, in this order:** -1. **Build on Windows** (`dotnet build` or open in VS) and fix any compile errors — I could not verify this. -2. Manually test in the running app: - - Toggle "Use custom DNS server" → Edit button opens dialog with fixed read-only "DNS server(s)" name → add/edit/remove IP:port entries → Save → label updates, `Network_CustomDNSServers` persists across restart. - - Verify hostname-only entries are rejected in that dialog (IP-only enforcement). - - Toggle "Add DNS suffix (primary) to hostname" / "Use custom DNS suffix" + suffix textbox enable/disable interplay (mirrors DNS Lookup settings UI — should look/behave identically). - - Actually resolve a bare hostname (e.g. via Ping or IP Scanner host range) with suffix enabled and confirm the suffix gets appended and resolution works; confirm FQDNs and PTR/reverse lookups are unaffected. - - Test the settings-upgrade migration path: hand-edit/restore an old settings JSON with a populated `Network_CustomDNSServer` string and no `Network_CustomDNSServers`, bump the settings version below `2026.8.17.0`, launch, and confirm it migrates correctly into the new list at port 53. -3. **Confirm/adjust the migration version number** `2026.8.17.0` in `SettingsManager.cs` (`Upgrade()` dispatcher + `UpgradeTo_2026_8_17_0()`) to match the actual intended release version if it differs from today's date-based guess. -4. Consider whether `Website/docs/application/dns-lookup.md` or other doc pages should cross-reference the new global suffix setting (only `Website/docs/settings/network.md` was updated). -5. Nothing was committed — review the diff (`git diff`) and commit when satisfied. - -## Explicit scope notes (things deliberately NOT done) - -- DNS Lookup's own settings/behavior were **not changed** — only read as a reference pattern to copy. -- No new localization strings were added anywhere; every label reuses existing `Strings.*`/`StaticStrings.*` keys (chosen deliberately to avoid needing a Transifex sync for 17 languages). If that turns out to read awkwardly in the UI (e.g. the dialog's read-only "Name" field showing "DNS server(s)"), that's a place a dedicated new string could be added later. -- Global DNS servers remain **IP-only by design** (not a limitation to relax casually) — `DNSClient.Configure()` does a hard `IPAddress.Parse()`, so allowing hostnames there would require adding a resolution step first. From 5282dc3a5d45f0922a134a5e9dbe28179be1825c Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:06:49 +0200 Subject: [PATCH 06/13] Feature: DNS server --- .../Network/DNSLookup.cs | 4 +- Source/NETworkManager.Utilities/DNSClient.cs | 13 +++--- .../DNSClientHelper.cs | 41 ++++++++++++++++++- Source/NETworkManager/MainWindow.xaml.cs | 2 +- Website/docs/application/dns-lookup.md | 2 +- 5 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Source/NETworkManager.Models/Network/DNSLookup.cs b/Source/NETworkManager.Models/Network/DNSLookup.cs index b1162a18cc..cb18346b20 100644 --- a/Source/NETworkManager.Models/Network/DNSLookup.cs +++ b/Source/NETworkManager.Models/Network/DNSLookup.cs @@ -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; @@ -24,7 +24,7 @@ public DNSLookup(DNSLookupSettings settings, IEnumerable d { _suffix = _settings.UseCustomDNSSuffix ? _settings.CustomDNSSuffix - : IPGlobalProperties.GetIPGlobalProperties().DomainName; + : DNSClientHelper.DetectDNSSuffix(); _addSuffix = !string.IsNullOrEmpty(_suffix); } diff --git a/Source/NETworkManager.Utilities/DNSClient.cs b/Source/NETworkManager.Utilities/DNSClient.cs index 8a32bcb19b..c33185780d 100644 --- a/Source/NETworkManager.Utilities/DNSClient.cs +++ b/Source/NETworkManager.Utilities/DNSClient.cs @@ -43,11 +43,9 @@ public class DNSClient : SingletonBase /// public void Configure(DNSClientSettings settings) { - _settings = settings; - - _addSuffix = _settings.AddDNSSuffix && !string.IsNullOrEmpty(_settings.DNSSuffix); - Log.Debug("Configure - Configuring DNS client..."); + + _settings = settings; if (_settings.UseCustomDNSServers) { @@ -63,7 +61,7 @@ public void Configure(DNSClientSettings settings) } Log.Debug("Configure - Creating LookupClient with custom DNS servers..."); - _client = new LookupClient(new LookupClientOptions(servers.ToArray())); + _client = new LookupClient(new LookupClientOptions([.. servers])); } else { @@ -71,6 +69,11 @@ public void Configure(DNSClientSettings settings) _client = new LookupClient(); } + _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."); + Log.Debug("Configure - DNS client configured."); _isConfigured = true; } diff --git a/Source/NETworkManager.Utilities/DNSClientHelper.cs b/Source/NETworkManager.Utilities/DNSClientHelper.cs index 6c420d8fe2..d4244c19e1 100644 --- a/Source/NETworkManager.Utilities/DNSClientHelper.cs +++ b/Source/NETworkManager.Utilities/DNSClientHelper.cs @@ -1,9 +1,48 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Net.NetworkInformation; +using System.Threading.Tasks; namespace NETworkManager.Utilities; public static class DNSClientHelper { + /// + /// Detect the DNS suffix to use for suffix-appending. Prefers the Windows "Primary DNS Suffix" + /// (), falling back to the connection-specific suffix + /// of an active network adapter if no primary suffix is configured (e.g. machine is not domain-joined + /// or the primary suffix was cleared manually), which mirrors what shows up under "DNS Suffix Search + /// List" in ipconfig /all in that case. + /// + public static string DetectDNSSuffix() + { + var suffix = IPGlobalProperties.GetIPGlobalProperties().DomainName; + + if (!string.IsNullOrWhiteSpace(suffix)) + return suffix; + + // Rank candidates so the adapter most likely to be "the" active connection is tried first: + // routed (has a gateway) > wired > wireless > other > faster link speed. + var prioritizedAdapters = NetworkInterface.GetAllNetworkInterfaces() + .Where(nic => nic.OperationalStatus == OperationalStatus.Up && + nic.NetworkInterfaceType != NetworkInterfaceType.Loopback && + nic.NetworkInterfaceType != NetworkInterfaceType.Tunnel) + .OrderByDescending(nic => nic.GetIPProperties().GatewayAddresses.Count > 0) + .ThenByDescending(nic => GetInterfaceTypePriority(nic.NetworkInterfaceType)) + .ThenByDescending(nic => nic.Speed); + + return prioritizedAdapters + .Select(nic => nic.GetIPProperties().DnsSuffix) + .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s)) + ?? string.Empty; + } + + private static int GetInterfaceTypePriority(NetworkInterfaceType type) => type switch + { + NetworkInterfaceType.Ethernet => 2, + NetworkInterfaceType.Wireless80211 => 1, + _ => 0 + }; + public static async Task ResolveAorAaaaAsync(string query, bool preferIPv4) { DNSClientResultIPAddress firstResult = null; diff --git a/Source/NETworkManager/MainWindow.xaml.cs b/Source/NETworkManager/MainWindow.xaml.cs index 095d926df2..0a426a6bba 100644 --- a/Source/NETworkManager/MainWindow.xaml.cs +++ b/Source/NETworkManager/MainWindow.xaml.cs @@ -1922,7 +1922,7 @@ .. SettingsManager.Current.Network_CustomDNSServers dnsSettings.AddDNSSuffix = true; dnsSettings.DNSSuffix = SettingsManager.Current.Network_UseCustomDNSSuffix ? SettingsManager.Current.Network_CustomDNSSuffix?.TrimStart('.') - : IPGlobalProperties.GetIPGlobalProperties().DomainName; + : DNSClientHelper.DetectDNSSuffix(); } DNSClient.GetInstance().Configure(dnsSettings); diff --git a/Website/docs/application/dns-lookup.md b/Website/docs/application/dns-lookup.md index c6a4a17461..3203422f66 100644 --- a/Website/docs/application/dns-lookup.md +++ b/Website/docs/application/dns-lookup.md @@ -119,7 +119,7 @@ You can also use the Hotkeys `F2` (`edit`) or `Del` (`delete`) on a selected DNS ### Add DNS suffix (primary) to hostname -Add the primary DNS suffix to the hostname. +Add the primary DNS suffix to the hostname. If no primary DNS suffix is configured (e.g. the computer is not domain-joined), the connection-specific DNS suffix of the active network adapter is used instead. **Type:** `Boolean` From 8a3a2a20f712e099bfc0efcc95f07c38e2b97cdf Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:16:55 +0200 Subject: [PATCH 07/13] Docs: #3591 --- Website/docs/changelog/next-release.md | 9 +++++++ Website/docs/settings/network.md | 34 +++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 33e29c9986..53caf4025e 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -29,6 +29,11 @@ Release date: **xx.xx.2026** - New **Map** view below the hop list, visualizing each resolved hop's geolocation on an offline world map. Consecutive hops are connected with curved, directional arrows; hovering a marker shows its location, ISP/ASN, hostname, IP address and average round-trip time, while hovering an arrow shows the source and destination location of that segment. The map supports mouse-wheel zoom and drag-to-pan, and can be collapsed via a toggle button on the map itself, similar to the Profiles panel. The map is only shown if **Check IP geolocation** and the new **Show map** setting are both enabled, since hops need a resolved geolocation to be plotted. [#3520](https://github.com/BornToBeRoot/NETworkManager/pull/3520) +**Settings** + +- New global **DNS suffix** options in [Settings > Network](../settings/network.md): **Add DNS suffix (primary) to hostname** appends the Windows primary (or, if none is set, the active network adapter's connection-specific) DNS suffix to a bare hostname before resolving it, and **Use custom DNS suffix** lets you override it with a suffix of your own. Previously only available in DNS Lookup, this now also applies to every other tool that resolves hostnames through NETworkManager's shared DNS resolver (e.g. Ping, Traceroute, Port Scanner, IP Scanner, NTP Lookup) — DNS Lookup stays independent. [#3591](https://github.com/BornToBeRoot/NETworkManager/pull/3591) +- **DNS server(s)** in [Settings > Network](../settings/network.md) is now a list of servers (IP address + port), configured via an **Edit DNS server** dialog, replacing the previous single semicolon-separated, port-53-only text field. [#3591](https://github.com/BornToBeRoot/NETworkManager/pull/3591) + ## Improvements - The collapsed/expanded state of profile groups (e.g. **linux-server**) is now remembered per profile file and shared across all tools, instead of resetting every time you switch tools or restart the application. [#3539](https://github.com/BornToBeRoot/NETworkManager/pull/3539) @@ -68,6 +73,10 @@ Release date: **xx.xx.2026** - Fixed the DNS status (Computer/Router/Internet) in the **Network Connection** widget showing as an error when no PTR record exists for the address, which is common and expected for private IP ranges. This is now shown as informational instead of critical. [#3553](https://github.com/BornToBeRoot/NETworkManager/pull/3553) - Fixed a race condition in the **Network Connection** widget where results from a superseded check could overwrite the results of a newer, still-running check after quickly reopening the widget. [#3553](https://github.com/BornToBeRoot/NETworkManager/pull/3553) +**DNS Lookup** + +- Fixed **Add DNS suffix (primary) to hostname** silently appending no suffix at all on machines without a Windows primary DNS suffix configured (e.g. not domain-joined), even though a usable suffix was available via the active network adapter's connection-specific DNS suffix (shown under **DNS Suffix Search List** in `ipconfig /all`). It now falls back to that adapter's suffix in this case. [#3591](https://github.com/BornToBeRoot/NETworkManager/pull/3591) + **IP Scanner** - Fixed NetBIOS lookups (computer name, domain/workgroup, user name) not starting until a host's entire port scan had finished, since the port scan wasn't actually running asynchronously despite being awaited alongside it. Ping, port scan, and NetBIOS resolution now genuinely run concurrently for every host. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564) diff --git a/Website/docs/settings/network.md b/Website/docs/settings/network.md index 9505c6cd45..72feae670a 100644 --- a/Website/docs/settings/network.md +++ b/Website/docs/settings/network.md @@ -1,7 +1,7 @@ --- sidebar_position: 4 -description: "Configure custom DNS servers used by NETworkManager for name resolution across all network tools." -keywords: [NETworkManager, network settings, DNS server configuration, custom DNS, name resolution] +description: "Configure the global network settings for NETworkManager, including custom DNS servers, DNS suffix behavior, and hostname resolution protocol preference." +keywords: [NETworkManager, network settings, DNS server configuration, custom DNS, DNS suffix, name resolution] --- # Network @@ -16,13 +16,39 @@ Enables or disables the custom DNS server(s) for all DNS queries. If disabled, t ### DNS server(s) -A semicolon-separated list of IP addresses of DNS servers to be used for DNS queries when [Use custom DNS server](#use-custom-dns-server) is enabled. +The IP addresses (and ports) of the DNS servers to be used for DNS queries when [Use custom DNS server](#use-custom-dns-server) is enabled. Configured via the **Edit DNS server** button. + +**Type:** `List of ServerConnectionInfo (IP address + port)` + +**Default:** `Empty` + +**Example:** `1.1.1.1:53; 1.0.0.1:53` + +### Add DNS suffix (primary) to hostname + +Enables or disables appending a DNS suffix to a bare hostname (one without a dot) before it is resolved. This applies to all tools that resolve hostnames through NETworkManager's DNS resolver (e.g. Ping, Traceroute, Port Scanner, IP Scanner, NTP Lookup). Fully-qualified hostnames and reverse (PTR) lookups are not affected. If disabled, hostnames are resolved as entered. If enabled, either the primary DNS suffix configured in Windows or the [custom DNS suffix](#use-custom-dns-suffix) is appended. If no primary DNS suffix is configured (e.g. the computer is not domain-joined), the connection-specific DNS suffix of the active network adapter is used instead. + +**Type:** `Boolean` + +**Default:** `Enabled` + +### Use custom DNS suffix + +Enables or disables the use of a custom DNS suffix instead of the primary DNS suffix configured in Windows, when [Add DNS suffix (primary) to hostname](#add-dns-suffix-primary-to-hostname) is enabled. + +**Type:** `Boolean` + +**Default:** `Disabled` + +### DNS suffix + +The custom DNS suffix to append to a hostname when [Use custom DNS suffix](#use-custom-dns-suffix) is enabled. **Type:** `String` **Default:** `Empty` -**Example:** `1.1.1.1; 1.0.0.1` +**Example:** `example.com` ### Preffered protocol when resolving hostnames: From 1fce95eec3b1bc3b515118db3945b6db069a69d9 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:30:37 +0200 Subject: [PATCH 08/13] Chore: Copilot feedback 1 --- .../NETworkManager.Utilities/RegexHelper.cs | 8 ++++++++ .../DNSSuffixValidator.cs | 19 +++++++++++++++++++ Source/NETworkManager/MainWindow.xaml.cs | 10 ++++------ ...erverConnectionInfoProfileChildWindow.xaml | 1 + .../Views/SettingsNetworkView.xaml | 2 +- 5 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 Source/NETworkManager.Validators/DNSSuffixValidator.cs diff --git a/Source/NETworkManager.Utilities/RegexHelper.cs b/Source/NETworkManager.Utilities/RegexHelper.cs index 8462c2eedf..93a45adaa9 100644 --- a/Source/NETworkManager.Utilities/RegexHelper.cs +++ b/Source/NETworkManager.Utilities/RegexHelper.cs @@ -109,6 +109,14 @@ public static partial class RegexHelper [GeneratedRegex($@"^{HostnameOrDomainValues}$")] public static partial Regex HostnameOrDomainRegex(); + /// + /// Provides a compiled regular expression that matches a DNS suffix, with an optional leading dot, like + /// "example.com" or ".example.com". + /// + /// A instance that matches valid DNS suffixes. + [GeneratedRegex($@"^\.?{HostnameOrDomainValues}$")] + public static partial Regex DNSSuffixRegex(); + /// /// Creates a regular expression that matches a local directory path or one using environment variables, /// like "C:\Temp", "C:\My Settings", "%AppData%\settings". diff --git a/Source/NETworkManager.Validators/DNSSuffixValidator.cs b/Source/NETworkManager.Validators/DNSSuffixValidator.cs new file mode 100644 index 0000000000..1eca31fb68 --- /dev/null +++ b/Source/NETworkManager.Validators/DNSSuffixValidator.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using System.Windows.Controls; +using NETworkManager.Localization.Resources; +using NETworkManager.Utilities; + +namespace NETworkManager.Validators; + +public class DNSSuffixValidator : ValidationRule +{ + public override ValidationResult Validate(object value, CultureInfo cultureInfo) + { + if (value is not string suffix || string.IsNullOrEmpty(suffix)) + return new ValidationResult(false, Strings.FieldCannotBeEmpty); + + return RegexHelper.DNSSuffixRegex().IsMatch(suffix) + ? ValidationResult.ValidResult + : new ValidationResult(false, Strings.EnterValidDomain); + } +} diff --git a/Source/NETworkManager/MainWindow.xaml.cs b/Source/NETworkManager/MainWindow.xaml.cs index 0a426a6bba..5b3385e14c 100644 --- a/Source/NETworkManager/MainWindow.xaml.cs +++ b/Source/NETworkManager/MainWindow.xaml.cs @@ -1949,12 +1949,10 @@ private async void OnNetworkHasChanged() Log.Info("Network availability or address has changed!"); - // Update DNS server if network changed - if (!SettingsManager.Current.Network_UseCustomDNSServer) - { - Log.Info("Update Windows default DNS servers..."); - DNSClient.GetInstance().UpdateWindowsDNSSever(); - } + // Reconfigure the DNS client if network changed - re-detects the Windows default DNS servers + // and, if enabled, the auto-detected DNS suffix (both of which can differ on the new network). + Log.Info("Update DNS server configuration..."); + ConfigureDNSServer(); // Show status window on network change if (SettingsManager.Current.Status_ShowWindowOnNetworkChange) diff --git a/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml b/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml index 32723cdc14..158d75102c 100644 --- a/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml +++ b/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml @@ -191,6 +191,7 @@ + diff --git a/Source/NETworkManager/Views/SettingsNetworkView.xaml b/Source/NETworkManager/Views/SettingsNetworkView.xaml index 8ac7945595..b8599cbad5 100644 --- a/Source/NETworkManager/Views/SettingsNetworkView.xaml +++ b/Source/NETworkManager/Views/SettingsNetworkView.xaml @@ -68,7 +68,7 @@ - + From 36d2acc25df867d6e2bf33e195764fad6b34f39c Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:42:00 +0200 Subject: [PATCH 09/13] Chore: Use state for resolver --- Source/NETworkManager.Utilities/DNSClient.cs | 70 +++++++++---------- .../DNSClientHelper.cs | 19 +++-- .../DNSClientSettings.cs | 3 + 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/Source/NETworkManager.Utilities/DNSClient.cs b/Source/NETworkManager.Utilities/DNSClient.cs index c33185780d..96c762fbed 100644 --- a/Source/NETworkManager.Utilities/DNSClient.cs +++ b/Source/NETworkManager.Utilities/DNSClient.cs @@ -18,9 +18,13 @@ public class DNSClient : SingletonBase private const string NotConfiguredMessage = "DNS client is not configured. Call Configure() first."; /// - /// 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 + /// call never observe a torn combination (e.g. the old suffix flag with the new lookup client). + /// Relies on not being mutated after being passed to + /// - see the note on itself. /// - private LookupClient _client; + private sealed record ResolverState(LookupClient Client, bool AddSuffix, DNSClientSettings Settings); /// /// Indicates if the DNS client is configured. @@ -28,14 +32,9 @@ public class DNSClient : SingletonBase private bool _isConfigured; /// - /// Store the current DNS settings. + /// Current resolver state (lookup client + DNS suffix behavior), swapped atomically on configure. /// - private DNSClientSettings _settings; - - /// - /// Indicates if the DNS suffix should be added to a hostname without a dot before resolving it. - /// - private bool _addSuffix; + private ResolverState _state; /// /// Method to configure the DNS client. @@ -44,50 +43,42 @@ public class DNSClient : SingletonBase public void Configure(DNSClientSettings settings) { Log.Debug("Configure - Configuring DNS client..."); - - _settings = settings; - if (_settings.UseCustomDNSServers) + LookupClient client; + + if (settings.UseCustomDNSServers) { Log.Debug("Configure - Using custom DNS servers..."); // Setup custom DNS servers List 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])); + client = new LookupClient(new LookupClientOptions([.. servers])); } else { Log.Debug("Configure - Creating LookupClient with Windows default DNS servers..."); - _client = new LookupClient(); + client = new LookupClient(); } - _addSuffix = _settings.AddDNSSuffix && !string.IsNullOrEmpty(_settings.DNSSuffix); - Log.Debug(_addSuffix - ? $"Configure - DNS suffix will be added to hostnames without a dot: {_settings.DNSSuffix}" + 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; } - /// - /// Method to update the (Windows) name servers of the DNS client - /// when they may have changed due to a network update. - /// - public void UpdateWindowsDNSSever() - { - Log.Debug("UpdateWindowsDNSSever - Recreating LookupClient with with Windows default DNS servers..."); - _client = new LookupClient(); - } - /// /// Resolve an IPv4 address from a hostname or FQDN. /// @@ -98,11 +89,13 @@ public async Task ResolveAAsync(string query) if (!_isConfigured) throw new DNSClientNotConfiguredException(NotConfiguredMessage); - query = AddDNSSuffixIfConfigured(query); + 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 @@ -143,11 +136,13 @@ public async Task ResolveAaaaAsync(string query) if (!_isConfigured) throw new DNSClientNotConfiguredException(NotConfiguredMessage); - query = AddDNSSuffixIfConfigured(query); + 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 @@ -190,7 +185,7 @@ public async Task 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 @@ -233,7 +228,7 @@ public async Task 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 @@ -274,11 +269,12 @@ public async Task ResolvePtrAsync(IPAddress ipAddress) /// FQDNs (containing a dot) are returned unchanged. /// /// Hostname or FQDN as string like "example.com". + /// Resolver state snapshot captured at the start of the resolve call. /// Query with the DNS suffix appended, if configured and applicable. - private string AddDNSSuffixIfConfigured(string query) + private static string AddDNSSuffixIfConfigured(string query, ResolverState state) { - return _addSuffix && !string.IsNullOrEmpty(query) && !query.Contains('.') - ? $"{query}.{_settings.DNSSuffix}" + return state.AddSuffix && !string.IsNullOrEmpty(query) && !query.Contains('.') + ? $"{query}.{state.Settings.DNSSuffix}" : query; } diff --git a/Source/NETworkManager.Utilities/DNSClientHelper.cs b/Source/NETworkManager.Utilities/DNSClientHelper.cs index d4244c19e1..7c7f2bc084 100644 --- a/Source/NETworkManager.Utilities/DNSClientHelper.cs +++ b/Source/NETworkManager.Utilities/DNSClientHelper.cs @@ -20,14 +20,19 @@ public static string DetectDNSSuffix() if (!string.IsNullOrWhiteSpace(suffix)) return suffix; - // Rank candidates so the adapter most likely to be "the" active connection is tried first: - // routed (has a gateway) > wired > wireless > other > faster link speed. + // Rank candidates so the adapter most likely to be "the" relevant connection is tried first: + // VPN/tunnel > wired > wireless > other, then routed (has a gateway), then faster link speed. + // VPN ranks above wired/wireless because it's usually why suffix resolution matters in the first + // place (reaching internal/corporate hostnames), and a split-tunnel VPN commonly has no default + // gateway at all, so gateway-presence can't be the primary key without losing to e.g. a home + // Ethernet connection. A candidate is only ever picked if it has a real suffix (see below), so + // ranking a VPN/tunnel first doesn't risk picking up an irrelevant OS-internal pseudo-tunnel + // (Teredo, ISATAP, ...) - those essentially never carry a connection-specific DNS suffix. var prioritizedAdapters = NetworkInterface.GetAllNetworkInterfaces() .Where(nic => nic.OperationalStatus == OperationalStatus.Up && - nic.NetworkInterfaceType != NetworkInterfaceType.Loopback && - nic.NetworkInterfaceType != NetworkInterfaceType.Tunnel) - .OrderByDescending(nic => nic.GetIPProperties().GatewayAddresses.Count > 0) - .ThenByDescending(nic => GetInterfaceTypePriority(nic.NetworkInterfaceType)) + nic.NetworkInterfaceType != NetworkInterfaceType.Loopback) + .OrderByDescending(nic => GetInterfaceTypePriority(nic.NetworkInterfaceType)) + .ThenByDescending(nic => nic.GetIPProperties().GatewayAddresses.Count > 0) .ThenByDescending(nic => nic.Speed); return prioritizedAdapters @@ -38,6 +43,8 @@ public static string DetectDNSSuffix() private static int GetInterfaceTypePriority(NetworkInterfaceType type) => type switch { + NetworkInterfaceType.Tunnel => 3, + NetworkInterfaceType.Ppp => 3, NetworkInterfaceType.Ethernet => 2, NetworkInterfaceType.Wireless80211 => 1, _ => 0 diff --git a/Source/NETworkManager.Utilities/DNSClientSettings.cs b/Source/NETworkManager.Utilities/DNSClientSettings.cs index ebac3a93aa..cec407e7ca 100644 --- a/Source/NETworkManager.Utilities/DNSClientSettings.cs +++ b/Source/NETworkManager.Utilities/DNSClientSettings.cs @@ -4,6 +4,9 @@ namespace NETworkManager.Utilities; /// /// Class is used to store settings for the . +/// retains the instance passed to and reads +/// it from concurrent resolve calls without further synchronization - do not mutate an instance after +/// passing it to ; construct a new one instead. /// public class DNSClientSettings { From ea235349c7329d1d75060262cdab410e54f79c8d Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:57:01 +0200 Subject: [PATCH 10/13] Chore: Copilot feedback --- Source/NETworkManager.Models/Network/DNSLookup.cs | 10 ++++++++-- .../Network/ServerConnectionInfo.cs | 9 ++++++--- Source/NETworkManager.Utilities/DNSClient.cs | 5 ++++- Website/docs/changelog/next-release.md | 2 +- Website/docs/settings/network.md | 2 +- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Source/NETworkManager.Models/Network/DNSLookup.cs b/Source/NETworkManager.Models/Network/DNSLookup.cs index cb18346b20..3592e50549 100644 --- a/Source/NETworkManager.Models/Network/DNSLookup.cs +++ b/Source/NETworkManager.Models/Network/DNSLookup.cs @@ -122,13 +122,19 @@ private IEnumerable GetDnsServer(IEnumerable d } /// - /// Append DNS suffix to hostname if not set. + /// Append DNS suffix to hostname if not set. IP literals are left unchanged - an IPv6 address like + /// "2001:db8::1" has no dot and would otherwise be mistaken for a bare hostname (the Host input + /// accepts hostnames and IP addresses alike, for any query type). /// /// List of hosts /// List of host with DNS suffix private IEnumerable GetHostsWithSuffix(IEnumerable hosts) { - return [.. hosts.Select(host => host.Contains('.') ? host : $"{host}.{_suffix}")]; + return + [ + .. hosts.Select(host => + host.Contains('.') || IPAddress.TryParse(host, out _) ? host : $"{host}.{_suffix}") + ]; } /// diff --git a/Source/NETworkManager.Models/Network/ServerConnectionInfo.cs b/Source/NETworkManager.Models/Network/ServerConnectionInfo.cs index 0a402771a6..8f40f3e13a 100644 --- a/Source/NETworkManager.Models/Network/ServerConnectionInfo.cs +++ b/Source/NETworkManager.Models/Network/ServerConnectionInfo.cs @@ -166,11 +166,14 @@ public static ServerConnectionInfo Parse(string input, int defaultPort, Transpor } /// - /// Returns a string that represents the current object. + /// Returns a string that represents the current object. IPv6 addresses are bracketed + /// ([::1]:53) so the address and port remain distinguishable. /// - /// Server:Port + /// Server:Port, or [Server]:Port for an IPv6 address. public override string ToString() { - return $"{Server}:{Port}"; + return IPAddress.TryParse(Server, out var ip) && ip.AddressFamily == AddressFamily.InterNetworkV6 + ? $"[{Server}]:{Port}" + : $"{Server}:{Port}"; } } \ No newline at end of file diff --git a/Source/NETworkManager.Utilities/DNSClient.cs b/Source/NETworkManager.Utilities/DNSClient.cs index 96c762fbed..388faa5ed2 100644 --- a/Source/NETworkManager.Utilities/DNSClient.cs +++ b/Source/NETworkManager.Utilities/DNSClient.cs @@ -273,7 +273,10 @@ public async Task ResolvePtrAsync(IPAddress ipAddress) /// Query with the DNS suffix appended, if configured and applicable. private static string AddDNSSuffixIfConfigured(string query, ResolverState state) { - return state.AddSuffix && !string.IsNullOrEmpty(query) && !query.Contains('.') + // Exclude IP literals - an IPv6 address like "2001:db8::1" has no dot and would otherwise be + // mistaken for a bare hostname (e.g. by the profile "Resolve" action, which allows IP literals). + return state.AddSuffix && !string.IsNullOrEmpty(query) && !query.Contains('.') && + !IPAddress.TryParse(query, out _) ? $"{query}.{state.Settings.DNSSuffix}" : query; } diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 53caf4025e..ff7e9e00da 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -31,7 +31,7 @@ Release date: **xx.xx.2026** **Settings** -- New global **DNS suffix** options in [Settings > Network](../settings/network.md): **Add DNS suffix (primary) to hostname** appends the Windows primary (or, if none is set, the active network adapter's connection-specific) DNS suffix to a bare hostname before resolving it, and **Use custom DNS suffix** lets you override it with a suffix of your own. Previously only available in DNS Lookup, this now also applies to every other tool that resolves hostnames through NETworkManager's shared DNS resolver (e.g. Ping, Traceroute, Port Scanner, IP Scanner, NTP Lookup) — DNS Lookup stays independent. [#3591](https://github.com/BornToBeRoot/NETworkManager/pull/3591) +- New global **DNS suffix** options in [Settings > Network](../settings/network.md): **Add DNS suffix (primary) to hostname** appends the Windows primary (or, if none is set, the active network adapter's connection-specific) DNS suffix to a bare hostname before resolving it, and **Use custom DNS suffix** lets you override it with a suffix of your own. Previously only available in DNS Lookup, this now also applies to every other tool that resolves hostnames through NETworkManager's shared DNS resolver (e.g. Ping, Traceroute, Port Scanner, IP Scanner, SNTP Lookup) — DNS Lookup stays independent. [#3591](https://github.com/BornToBeRoot/NETworkManager/pull/3591) - **DNS server(s)** in [Settings > Network](../settings/network.md) is now a list of servers (IP address + port), configured via an **Edit DNS server** dialog, replacing the previous single semicolon-separated, port-53-only text field. [#3591](https://github.com/BornToBeRoot/NETworkManager/pull/3591) ## Improvements diff --git a/Website/docs/settings/network.md b/Website/docs/settings/network.md index 72feae670a..f7cc49f5dc 100644 --- a/Website/docs/settings/network.md +++ b/Website/docs/settings/network.md @@ -26,7 +26,7 @@ The IP addresses (and ports) of the DNS servers to be used for DNS queries when ### Add DNS suffix (primary) to hostname -Enables or disables appending a DNS suffix to a bare hostname (one without a dot) before it is resolved. This applies to all tools that resolve hostnames through NETworkManager's DNS resolver (e.g. Ping, Traceroute, Port Scanner, IP Scanner, NTP Lookup). Fully-qualified hostnames and reverse (PTR) lookups are not affected. If disabled, hostnames are resolved as entered. If enabled, either the primary DNS suffix configured in Windows or the [custom DNS suffix](#use-custom-dns-suffix) is appended. If no primary DNS suffix is configured (e.g. the computer is not domain-joined), the connection-specific DNS suffix of the active network adapter is used instead. +Enables or disables appending a DNS suffix to a bare hostname (one without a dot) before it is resolved. This applies to all tools that resolve hostnames through NETworkManager's DNS resolver (e.g. Ping, Traceroute, Port Scanner, IP Scanner, SNTP Lookup). Fully-qualified hostnames and reverse (PTR) lookups are not affected. If disabled, hostnames are resolved as entered. If enabled, either the primary DNS suffix configured in Windows or the [custom DNS suffix](#use-custom-dns-suffix) is appended. If no primary DNS suffix is configured (e.g. the computer is not domain-joined), the connection-specific DNS suffix of the active network adapter is used instead. **Type:** `Boolean` From 64d1786a97a3d27dba67cd4b8f10931e8dae8d8c Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:27:25 +0200 Subject: [PATCH 11/13] Chore: Claude code review --- .../Network/DNSLookup.cs | 9 +-- Source/NETworkManager.Utilities/DNSClient.cs | 11 ++- .../DNSClientHelper.cs | 70 +++++++++++++------ .../Views/DNSLookupSettingsView.xaml | 2 +- 4 files changed, 57 insertions(+), 35 deletions(-) diff --git a/Source/NETworkManager.Models/Network/DNSLookup.cs b/Source/NETworkManager.Models/Network/DNSLookup.cs index 3592e50549..4c0ed2cc17 100644 --- a/Source/NETworkManager.Models/Network/DNSLookup.cs +++ b/Source/NETworkManager.Models/Network/DNSLookup.cs @@ -122,19 +122,14 @@ private IEnumerable GetDnsServer(IEnumerable d } /// - /// Append DNS suffix to hostname if not set. IP literals are left unchanged - an IPv6 address like - /// "2001:db8::1" has no dot and would otherwise be mistaken for a bare hostname (the Host input + /// 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). /// /// List of hosts /// List of host with DNS suffix private IEnumerable GetHostsWithSuffix(IEnumerable hosts) { - return - [ - .. hosts.Select(host => - host.Contains('.') || IPAddress.TryParse(host, out _) ? host : $"{host}.{_suffix}") - ]; + return [.. hosts.Select(host => DNSClientHelper.IsBareHostname(host) ? $"{host}.{_suffix}" : host)]; } /// diff --git a/Source/NETworkManager.Utilities/DNSClient.cs b/Source/NETworkManager.Utilities/DNSClient.cs index 388faa5ed2..a72103face 100644 --- a/Source/NETworkManager.Utilities/DNSClient.cs +++ b/Source/NETworkManager.Utilities/DNSClient.cs @@ -265,18 +265,15 @@ public async Task ResolvePtrAsync(IPAddress ipAddress) } /// - /// Appends the configured DNS suffix to a hostname without a dot (forward lookups only). - /// FQDNs (containing a dot) are returned unchanged. + /// Appends the configured DNS suffix to a bare hostname (forward lookups only). + /// FQDNs and IP literals are returned unchanged. /// - /// Hostname or FQDN as string like "example.com". + /// Hostname, FQDN, or IP address as string like "example.com". /// Resolver state snapshot captured at the start of the resolve call. /// Query with the DNS suffix appended, if configured and applicable. private static string AddDNSSuffixIfConfigured(string query, ResolverState state) { - // Exclude IP literals - an IPv6 address like "2001:db8::1" has no dot and would otherwise be - // mistaken for a bare hostname (e.g. by the profile "Resolve" action, which allows IP literals). - return state.AddSuffix && !string.IsNullOrEmpty(query) && !query.Contains('.') && - !IPAddress.TryParse(query, out _) + return state.AddSuffix && DNSClientHelper.IsBareHostname(query) ? $"{query}.{state.Settings.DNSSuffix}" : query; } diff --git a/Source/NETworkManager.Utilities/DNSClientHelper.cs b/Source/NETworkManager.Utilities/DNSClientHelper.cs index 7c7f2bc084..0d1913eba6 100644 --- a/Source/NETworkManager.Utilities/DNSClientHelper.cs +++ b/Source/NETworkManager.Utilities/DNSClientHelper.cs @@ -1,4 +1,7 @@ -using System.Linq; +using log4net; +using System; +using System.Linq; +using System.Net; using System.Net.NetworkInformation; using System.Threading.Tasks; @@ -6,6 +9,18 @@ namespace NETworkManager.Utilities; public static class DNSClientHelper { + private static readonly ILog Log = LogManager.GetLogger(typeof(DNSClientHelper)); + + /// + /// Determines whether a query is a bare hostname that a DNS suffix should be appended to: + /// not empty, has no dot (not already an FQDN), and isn't an IP address literal (e.g. a bare + /// IPv6 address like "2001:db8::1" has no dot and would otherwise be mistaken for a hostname). + /// + public static bool IsBareHostname(string query) + { + return !string.IsNullOrEmpty(query) && !query.Contains('.') && !IPAddress.TryParse(query, out _); + } + /// /// Detect the DNS suffix to use for suffix-appending. Prefers the Windows "Primary DNS Suffix" /// (), falling back to the connection-specific suffix @@ -20,25 +35,36 @@ public static string DetectDNSSuffix() if (!string.IsNullOrWhiteSpace(suffix)) return suffix; - // Rank candidates so the adapter most likely to be "the" relevant connection is tried first: - // VPN/tunnel > wired > wireless > other, then routed (has a gateway), then faster link speed. - // VPN ranks above wired/wireless because it's usually why suffix resolution matters in the first - // place (reaching internal/corporate hostnames), and a split-tunnel VPN commonly has no default - // gateway at all, so gateway-presence can't be the primary key without losing to e.g. a home - // Ethernet connection. A candidate is only ever picked if it has a real suffix (see below), so - // ranking a VPN/tunnel first doesn't risk picking up an irrelevant OS-internal pseudo-tunnel - // (Teredo, ISATAP, ...) - those essentially never carry a connection-specific DNS suffix. - var prioritizedAdapters = NetworkInterface.GetAllNetworkInterfaces() - .Where(nic => nic.OperationalStatus == OperationalStatus.Up && - nic.NetworkInterfaceType != NetworkInterfaceType.Loopback) - .OrderByDescending(nic => GetInterfaceTypePriority(nic.NetworkInterfaceType)) - .ThenByDescending(nic => nic.GetIPProperties().GatewayAddresses.Count > 0) - .ThenByDescending(nic => nic.Speed); - - return prioritizedAdapters - .Select(nic => nic.GetIPProperties().DnsSuffix) - .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s)) - ?? string.Empty; + try + { + // Rank candidates so the adapter most likely to be "the" relevant connection is tried first: + // VPN/tunnel > wired > wireless > other, then routed (has a gateway), then faster link speed. + // VPN ranks above wired/wireless because it's usually why suffix resolution matters in the first + // place (reaching internal/corporate hostnames), and a split-tunnel VPN commonly has no default + // gateway at all, so gateway-presence can't be the primary key without losing to e.g. a home + // Ethernet connection. A candidate is only ever picked if it has a real suffix (see below), so + // ranking a VPN/tunnel first doesn't risk picking up an irrelevant OS-internal pseudo-tunnel + // (Teredo, ISATAP, ...) - those essentially never carry a connection-specific DNS suffix. + var prioritizedAdapters = NetworkInterface.GetAllNetworkInterfaces() + .Where(nic => nic.OperationalStatus == OperationalStatus.Up && + nic.NetworkInterfaceType != NetworkInterfaceType.Loopback) + .Select(nic => (Nic: nic, Properties: nic.GetIPProperties())) + .OrderByDescending(x => GetInterfaceTypePriority(x.Nic.NetworkInterfaceType)) + .ThenByDescending(x => x.Properties.GatewayAddresses.Count > 0) + .ThenByDescending(x => x.Nic.Speed); + + return prioritizedAdapters + .Select(x => x.Properties.DnsSuffix) + .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s)) + ?? string.Empty; + } + catch (Exception ex) + { + // Best-effort fallback - an adapter can disappear (USB NIC unplugged, VPN tunnel torn down) + // while enumerating, throwing from GetIPProperties()/.Speed. Don't let that crash the caller. + Log.Warn("DetectDNSSuffix - Could not enumerate network adapters for DNS suffix detection.", ex); + return string.Empty; + } } private static int GetInterfaceTypePriority(NetworkInterfaceType type) => type switch @@ -46,6 +72,10 @@ public static string DetectDNSSuffix() NetworkInterfaceType.Tunnel => 3, NetworkInterfaceType.Ppp => 3, NetworkInterfaceType.Ethernet => 2, + NetworkInterfaceType.FastEthernetT => 2, + NetworkInterfaceType.FastEthernetFx => 2, + NetworkInterfaceType.GigabitEthernet => 2, + NetworkInterfaceType.Ethernet3Megabit => 2, NetworkInterfaceType.Wireless80211 => 1, _ => 0 }; diff --git a/Source/NETworkManager/Views/DNSLookupSettingsView.xaml b/Source/NETworkManager/Views/DNSLookupSettingsView.xaml index f1f1fb802f..702bc0165d 100644 --- a/Source/NETworkManager/Views/DNSLookupSettingsView.xaml +++ b/Source/NETworkManager/Views/DNSLookupSettingsView.xaml @@ -116,7 +116,7 @@ - + From b9e24217d3240a661747a575963663f8fca629fa Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:39:16 +0200 Subject: [PATCH 12/13] Fix: Don't close dialog inside textbox --- ...erverConnectionInfoProfileChildWindow.xaml | 6 ++-- ...erConnectionInfoProfileChildWindow.xaml.cs | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml b/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml index 158d75102c..167c088ab1 100644 --- a/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml +++ b/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml @@ -107,7 +107,8 @@ + mah:TextBoxHelper.Watermark="{Binding ServerWatermark}" + PreviewKeyDown="TextBoxServerOrPort_PreviewKeyDown"> @@ -126,7 +127,8 @@ + mah:TextBoxHelper.Watermark="{Binding PortWatermark}" + PreviewKeyDown="TextBoxServerOrPort_PreviewKeyDown"> diff --git a/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml.cs b/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml.cs index 225345fe59..cdd803ec44 100644 --- a/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml.cs +++ b/Source/NETworkManager/Views/ServerConnectionInfoProfileChildWindow.xaml.cs @@ -2,6 +2,7 @@ using System; using System.Windows; using System.Windows.Controls; +using System.Windows.Input; using System.Windows.Threading; namespace NETworkManager.Views; @@ -19,10 +20,18 @@ private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e) Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate { + // Focus() alone doesn't move the caret - place it after any existing text (e.g. when + // editing an existing profile) instead of leaving it at the start. if (isNameReadOnly) + { TextBoxServer.Focus(); + TextBoxServer.CaretIndex = TextBoxServer.Text.Length; + } else + { TextBoxName.Focus(); + TextBoxName.CaretIndex = TextBoxName.Text.Length; + } })); } @@ -31,4 +40,31 @@ private void ContextMenu_Opened(object sender, RoutedEventArgs e) if (sender is ContextMenu menu) menu.DataContext = (ServerConnectionInfoProfileViewModel)DataContext; } + + /// + /// Pressing enter in the server or port textbox adds the server (like clicking the Add button) + /// instead of triggering the window's default button (Save), as long as a server is entered. + /// If the server field is empty, falls through to the default behavior (Save, if enabled) instead. + /// Note: once a server is entered, enter is always intercepted here - even if currently invalid - + /// so it can never fall through to Save, which is enabled independently of the server/port + /// textboxes' validity and would otherwise silently discard an in-progress, invalid entry. + /// + private void TextBoxServerOrPort_PreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key != Key.Enter) + return; + + if (DataContext is not ServerConnectionInfoProfileViewModel viewModel) + return; + + if (string.IsNullOrEmpty(viewModel.Server)) + return; + + e.Handled = true; + + if (Validation.GetHasError(TextBoxServer) || Validation.GetHasError(TextBoxPort)) + return; + + viewModel.AddServerCommand.Execute(null); + } } \ No newline at end of file From 231284f21615ef9cd48fa30a750fe967d9235b11 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:45:06 +0200 Subject: [PATCH 13/13] Chore: Minor bugfix --- Source/NETworkManager/Views/CustomCommandChildWindow.xaml.cs | 1 + Source/NETworkManager/Views/GroupChildWindow.xaml.cs | 1 + Source/NETworkManager/Views/PortProfileChildWindow.xaml.cs | 1 + Source/NETworkManager/Views/ProfileChildWindow.xaml.cs | 1 + Source/NETworkManager/Views/ProfileFileChildWindow.xaml.cs | 1 + Source/NETworkManager/Views/SNMPOIDProfileChildWindow.xaml.cs | 1 + 6 files changed, 6 insertions(+) diff --git a/Source/NETworkManager/Views/CustomCommandChildWindow.xaml.cs b/Source/NETworkManager/Views/CustomCommandChildWindow.xaml.cs index 9e4de57d7b..26698d7c44 100644 --- a/Source/NETworkManager/Views/CustomCommandChildWindow.xaml.cs +++ b/Source/NETworkManager/Views/CustomCommandChildWindow.xaml.cs @@ -15,6 +15,7 @@ private void ChildWindow_OnLoaded(object sender, System.Windows.RoutedEventArgs Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate { TextBoxName.Focus(); + TextBoxName.CaretIndex = TextBoxName.Text.Length; })); } } diff --git a/Source/NETworkManager/Views/GroupChildWindow.xaml.cs b/Source/NETworkManager/Views/GroupChildWindow.xaml.cs index 47375ed158..c2118f42b2 100644 --- a/Source/NETworkManager/Views/GroupChildWindow.xaml.cs +++ b/Source/NETworkManager/Views/GroupChildWindow.xaml.cs @@ -30,6 +30,7 @@ private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e) Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate { TextBoxName.Focus(); + TextBoxName.CaretIndex = TextBoxName.Text.Length; })); } diff --git a/Source/NETworkManager/Views/PortProfileChildWindow.xaml.cs b/Source/NETworkManager/Views/PortProfileChildWindow.xaml.cs index 37c8b78fe3..05ce4eed6d 100644 --- a/Source/NETworkManager/Views/PortProfileChildWindow.xaml.cs +++ b/Source/NETworkManager/Views/PortProfileChildWindow.xaml.cs @@ -16,6 +16,7 @@ private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e) Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate { TextBoxName.Focus(); + TextBoxName.CaretIndex = TextBoxName.Text.Length; })); } } \ No newline at end of file diff --git a/Source/NETworkManager/Views/ProfileChildWindow.xaml.cs b/Source/NETworkManager/Views/ProfileChildWindow.xaml.cs index aa19c9d22f..1123854d71 100644 --- a/Source/NETworkManager/Views/ProfileChildWindow.xaml.cs +++ b/Source/NETworkManager/Views/ProfileChildWindow.xaml.cs @@ -34,6 +34,7 @@ private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e) Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate { TextBoxName.Focus(); + TextBoxName.CaretIndex = TextBoxName.Text.Length; })); } diff --git a/Source/NETworkManager/Views/ProfileFileChildWindow.xaml.cs b/Source/NETworkManager/Views/ProfileFileChildWindow.xaml.cs index e883e3e8ea..30ad3d4ad0 100644 --- a/Source/NETworkManager/Views/ProfileFileChildWindow.xaml.cs +++ b/Source/NETworkManager/Views/ProfileFileChildWindow.xaml.cs @@ -16,6 +16,7 @@ private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e) Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate { TextBoxName.Focus(); + TextBoxName.CaretIndex = TextBoxName.Text.Length; })); } } \ No newline at end of file diff --git a/Source/NETworkManager/Views/SNMPOIDProfileChildWindow.xaml.cs b/Source/NETworkManager/Views/SNMPOIDProfileChildWindow.xaml.cs index 0b7368eca2..61dbe93416 100644 --- a/Source/NETworkManager/Views/SNMPOIDProfileChildWindow.xaml.cs +++ b/Source/NETworkManager/Views/SNMPOIDProfileChildWindow.xaml.cs @@ -16,6 +16,7 @@ private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e) Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate { TextBoxName.Focus(); + TextBoxName.CaretIndex = TextBoxName.Text.Length; })); } } \ No newline at end of file