From 5a35fda0ff796f4d68c964ebc4ac6bdaaba0fe30 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:36:19 +0100 Subject: [PATCH 01/13] Chore: Port Scanner: Clarify MaxRowChangesOneByOne comment Co-Authored-By: Claude Opus 5.5 --- .../ViewModels/PortScannerViewModel.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs index d92c6fc70c..8842aed6a3 100644 --- a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs @@ -36,9 +36,16 @@ public class PortScannerViewModel : ViewModelBase, IProfileManagerMinimal private static readonly ILog Log = LogManager.GetLogger(typeof(PortScannerViewModel)); /// - /// Maximum effort (changed rows x all rows) to remove / add the rows one by one when the results of hosts are - /// replaced (rescan, see ) or buffered results are added (see - /// ). E.g. 1,000 rows of 10,000 or 10 rows of 1,000,000. Above, with one reset. + /// Maximum effort to remove / add rows one by one when the results of hosts are replaced (rescan, see + /// ) or buffered results are added (see ). Above, the + /// results are replaced with one reset (sorts and groups all rows once, but collapsed groups and the scroll + /// position are lost). + /// + /// The effort is a product (changed rows x all rows), not a row count: WPF inserts / removes every single row in + /// its group of the sorted view with a linear search. Even one host with 1-65535 (max. 65,535 rows) exceeds it, + /// e.g. adding 500 rows to 20,000 rows (10.25M) is done with a reset, 100 rows to 60,000 (6M) one by one. With + /// multiple hosts, the results can have millions of rows (e.g. a /24 with 1-65535 and closed ports shown). + /// /// private const long MaxRowChangesOneByOne = 10_000_000; From 9238fa7a79ca526ef5e1cd31a81876c8da8ddc99 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:42:25 +0100 Subject: [PATCH 02/13] Feature: IP Scanner: More default ports to detect if a host is up Add 554 (RTSP), 631 (IPP), 8080, 8443 and 62078 (iPhone / iPad) to the default ports (16 ports, all scanned at the same time with the default 16 port threads). Settings with the old defaults (release and pre-releases) are migrated. Co-Authored-By: Claude Opus 5.5 --- Source/NETworkManager.Settings/GlobalStaticConfiguration.cs | 2 +- Source/NETworkManager.Settings/SettingsManager.cs | 4 +++- Website/docs/application/ip-scanner.md | 2 +- Website/docs/changelog/next-release.md | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index 9ddfd821b1..ac77d2dbbd 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -142,7 +142,7 @@ public static class GlobalStaticConfiguration public static int IPScanner_ICMPBuffer => 32; public static bool IPScanner_ResolveHostname => true; public static bool IPScanner_PortScanEnabled => true; - public static string IPScanner_PortScanPorts => "22; 53; 80; 135; 139; 389; 636; 443; 445; 3389; 9100"; + public static string IPScanner_PortScanPorts => "22; 53; 80; 135; 139; 389; 443; 445; 554; 631; 636; 3389; 8080; 8443; 9100; 62078"; public static int IPScanner_PortScanTimeout => 2000; public static int IPScanner_PortScanAttempts => 1; public static int IPScanner_MaxHostThreads => 128; diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 2dc5a326ff..ea9b6bde60 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -856,7 +856,9 @@ private static void UpgradeToLatest(Version version) Current.PortScanner_Timeout = GlobalStaticConfiguration.PortScanner_Timeout; } - if (Current.IPScanner_PortScanPorts == "22; 53; 80; 139; 389; 636; 443; 445; 3389") + // Old defaults (until 2026.7.7.0 and of the pre-releases since 2026.9.7.0) + if (Current.IPScanner_PortScanPorts is "22; 53; 80; 139; 389; 636; 443; 445; 3389" + or "22; 53; 80; 135; 139; 389; 636; 443; 445; 3389; 9100") { Log.Info($"Update \"IPScanner_PortScanPorts\" to \"{GlobalStaticConfiguration.IPScanner_PortScanPorts}\"..."); Current.IPScanner_PortScanPorts = GlobalStaticConfiguration.IPScanner_PortScanPorts; diff --git a/Website/docs/application/ip-scanner.md b/Website/docs/application/ip-scanner.md index 73520c03fa..cbb6ae711c 100644 --- a/Website/docs/application/ip-scanner.md +++ b/Website/docs/application/ip-scanner.md @@ -204,7 +204,7 @@ List of TCP ports to scan for each IP address. The button next to the input rest **Type:** `String` -**Default:** `22; 53; 80; 135; 139; 389; 636; 443; 445; 3389; 9100` +**Default:** `22; 53; 80; 135; 139; 389; 443; 445; 554; 631; 636; 3389; 8080; 8443; 9100; 62078` :::note diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 445e42c67b..3724162a84 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -66,7 +66,7 @@ Release date: **xx.xx.2026** - Added a live count of hosts up/down next to the **Result** header. Stays visible after the scan finishes. [#3572](https://github.com/BornToBeRoot/NETworkManager/pull/3572) - Host input now accepts newline-separated hosts (one per line, e.g. a column pasted from Excel), converted automatically to the semicolon-separated form. Thanks to [@dearmb](https://github.com/dearmb) [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568) - Host input now supports shorthand IPv4 ranges like `192.168.0.1-100`, in addition to the existing `192.168.0.0-192.168.0.100` and `192.168.[0-100].1` range formats. [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568) -- Added `135` (RPC) and `9100` (raw printing) to the default **Ports** list used to detect if a host is reachable. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564) +- Added `135` (RPC), `554` (RTSP, e.g. IP cameras), `631` (IPP, printers), `8080` / `8443` (web interfaces of devices and appliances), `9100` (raw printing) and `62078` (iPhone / iPad) to the default **Ports** list used to detect if a host is reachable, e.g. hosts that block ICMP. With 16 ports, all ports of a host are still scanned at the same time (**Max. concurrent port threads** `16`). Settings that are still on the old default are updated. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564) - Added a button next to **Ports** in the settings to restore the default ports. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - New default custom commands **Windows Explorer (shares)**, **Computer Management**, **Event Viewer** and **System Information** (for new settings; existing custom commands are not changed). [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - Faster scans with new defaults: all ports of a host are now scanned at the same time (**Max. concurrent port threads** `5` → `16`, max. `32`) and the timeouts are lower (**ICMP**, **port scan** and **NetBIOS** timeout `4000` → `2000` ms), so an unreachable host is finished after about 4 seconds (2 ICMP attempts) instead of up to 12 seconds. **Max. concurrent host threads** is lowered from `256` to `128` (max. `256` instead of `512`) to put less simultaneous load on the network. An empty `/24` network is scanned in about 8 seconds instead of about 50. Settings that are still on the old default are updated. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564) [#3653](https://github.com/BornToBeRoot/NETworkManager/pull/3653) From b37498054c3e6c73f2a4d84614c9a52e29c6d913 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:46:24 +0100 Subject: [PATCH 03/13] Feature: IP Scanner: Add FTP, Telnet, VNC, WinRM, AirPlay and Chromecast to the default ports Add 21, 23, 5900, 5985, 7000 and 8009 (22 ports, 2 rounds with the default 16 port threads, ~4 s for an unreachable host like 2 ICMP attempts). Co-Authored-By: Claude Opus 5.5 --- Source/NETworkManager.Settings/GlobalStaticConfiguration.cs | 2 +- Website/docs/application/ip-scanner.md | 2 +- Website/docs/changelog/next-release.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index ac77d2dbbd..d75f29bdf2 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -142,7 +142,7 @@ public static class GlobalStaticConfiguration public static int IPScanner_ICMPBuffer => 32; public static bool IPScanner_ResolveHostname => true; public static bool IPScanner_PortScanEnabled => true; - public static string IPScanner_PortScanPorts => "22; 53; 80; 135; 139; 389; 443; 445; 554; 631; 636; 3389; 8080; 8443; 9100; 62078"; + public static string IPScanner_PortScanPorts => "21; 22; 23; 53; 80; 135; 139; 389; 443; 445; 554; 631; 636; 3389; 5900; 5985; 7000; 8009; 8080; 8443; 9100; 62078"; public static int IPScanner_PortScanTimeout => 2000; public static int IPScanner_PortScanAttempts => 1; public static int IPScanner_MaxHostThreads => 128; diff --git a/Website/docs/application/ip-scanner.md b/Website/docs/application/ip-scanner.md index cbb6ae711c..edc565a2c1 100644 --- a/Website/docs/application/ip-scanner.md +++ b/Website/docs/application/ip-scanner.md @@ -204,7 +204,7 @@ List of TCP ports to scan for each IP address. The button next to the input rest **Type:** `String` -**Default:** `22; 53; 80; 135; 139; 389; 443; 445; 554; 631; 636; 3389; 8080; 8443; 9100; 62078` +**Default:** `21; 22; 23; 53; 80; 135; 139; 389; 443; 445; 554; 631; 636; 3389; 5900; 5985; 7000; 8009; 8080; 8443; 9100; 62078` :::note diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 3724162a84..853b7b7bbc 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -66,7 +66,7 @@ Release date: **xx.xx.2026** - Added a live count of hosts up/down next to the **Result** header. Stays visible after the scan finishes. [#3572](https://github.com/BornToBeRoot/NETworkManager/pull/3572) - Host input now accepts newline-separated hosts (one per line, e.g. a column pasted from Excel), converted automatically to the semicolon-separated form. Thanks to [@dearmb](https://github.com/dearmb) [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568) - Host input now supports shorthand IPv4 ranges like `192.168.0.1-100`, in addition to the existing `192.168.0.0-192.168.0.100` and `192.168.[0-100].1` range formats. [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568) -- Added `135` (RPC), `554` (RTSP, e.g. IP cameras), `631` (IPP, printers), `8080` / `8443` (web interfaces of devices and appliances), `9100` (raw printing) and `62078` (iPhone / iPad) to the default **Ports** list used to detect if a host is reachable, e.g. hosts that block ICMP. With 16 ports, all ports of a host are still scanned at the same time (**Max. concurrent port threads** `16`). Settings that are still on the old default are updated. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564) +- Added `21` (FTP, e.g. NAS), `23` (Telnet, e.g. older switches), `135` (RPC), `554` (RTSP, e.g. IP cameras), `631` (IPP, printers), `5900` (VNC), `5985` (WinRM), `7000` (AirPlay), `8009` (Chromecast), `8080` / `8443` (web interfaces of devices and appliances), `9100` (raw printing) and `62078` (iPhone / iPad) to the default **Ports** list used to detect if a host is reachable, e.g. hosts that block ICMP. An unreachable host is still finished after about 4 seconds (22 ports with **Max. concurrent port threads** `16`, like 2 ICMP attempts). Settings that are still on the old default are updated. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564) - Added a button next to **Ports** in the settings to restore the default ports. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - New default custom commands **Windows Explorer (shares)**, **Computer Management**, **Event Viewer** and **System Information** (for new settings; existing custom commands are not changed). [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - Faster scans with new defaults: all ports of a host are now scanned at the same time (**Max. concurrent port threads** `5` → `16`, max. `32`) and the timeouts are lower (**ICMP**, **port scan** and **NetBIOS** timeout `4000` → `2000` ms), so an unreachable host is finished after about 4 seconds (2 ICMP attempts) instead of up to 12 seconds. **Max. concurrent host threads** is lowered from `256` to `128` (max. `256` instead of `512`) to put less simultaneous load on the network. An empty `/24` network is scanned in about 8 seconds instead of about 50. Settings that are still on the old default are updated. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564) [#3653](https://github.com/BornToBeRoot/NETworkManager/pull/3653) From 30cc91230dac66653505f0a3f7354e1f07a6f803 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:59:32 +0100 Subject: [PATCH 04/13] Feature: Port Scanner: Own attempts, timeout and buffer for the ping before the scan Ping hosts before the scan used the timeout / attempts of the port scan and a fixed 32-byte buffer. New settings like the IP Scanner (default 2 attempts, 2000 ms, 32 bytes), disabled while the option is off. Co-Authored-By: Claude Opus 5.5 --- .../Resources/Strings.Designer.cs | 2 +- .../Resources/Strings.resx | 2 +- .../Network/PortScanner.cs | 10 +--- .../Network/PortScannerOptions.cs | 20 ++++++- .../GlobalStaticConfiguration.cs | 3 + .../NETworkManager.Settings/SettingsInfo.cs | 39 ++++++++++++ .../PortScannerSettingsViewModel.cs | 60 +++++++++++++++++++ .../ViewModels/PortScannerViewModel.cs | 5 ++ .../Views/PortScannerSettingsView.xaml | 17 +++++- Website/docs/application/port-scanner.md | 26 +++++++- Website/docs/changelog/next-release.md | 2 +- 11 files changed, 171 insertions(+), 15 deletions(-) diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 94b162c792..6b5f1c9412 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -5967,7 +5967,7 @@ public static string HelpMessage_PasswordNotDisplayedCanBeOverwritten { } /// - /// Looks up a localized string similar to Only hosts that answer a ping (ICMP echo, with the timeout and attempts above) are scanned. Faster for ranges with many unused IP addresses. Hosts that block ICMP (e.g. Windows with the firewall enabled) are not scanned.. + /// Looks up a localized string similar to Only hosts that answer a ping (ICMP echo, with the attempts, timeout and buffer below) are scanned. Faster for ranges with many unused IP addresses. Hosts that block ICMP (e.g. Windows with the firewall enabled) are not scanned.. /// public static string HelpMessage_PingHostsBeforeScan { get { diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 3fadda001b..5c83137090 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -5129,7 +5129,7 @@ Do you want to start the scan? Ping hosts before the scan - Only hosts that answer a ping (ICMP echo, with the timeout and attempts above) are scanned. Faster for ranges with many unused IP addresses. Hosts that block ICMP (e.g. Windows with the firewall enabled) are not scanned. + Only hosts that answer a ping (ICMP echo, with the attempts, timeout and buffer below) are scanned. Faster for ranges with many unused IP addresses. Hosts that block ICMP (e.g. Windows with the firewall enabled) are not scanned. {0} host(s) did not answer the ping and were not scanned. diff --git a/Source/NETworkManager.Models/Network/PortScanner.cs b/Source/NETworkManager.Models/Network/PortScanner.cs index 0aa38b6cb4..72be0ce9a0 100644 --- a/Source/NETworkManager.Models/Network/PortScanner.cs +++ b/Source/NETworkManager.Models/Network/PortScanner.cs @@ -63,12 +63,6 @@ public PortScanner(PortScannerOptions options) private readonly InterlockedCounter _portsTimedOut = new(); private readonly InterlockedCounter _hostsSkipped = new(); - /// - /// Data of the ping before the scan (see ), like the - /// default buffer of the IP Scanner. - /// - private static readonly byte[] PingBuffer = new byte[32]; - // Closed and timed out ports per host, see GetClosedAndTimedOutPortsPerHost private readonly ConcurrentDictionary _portsPerHost = new(); @@ -399,8 +393,8 @@ await Parallel.ForEachAsync(hosts, pingParallelOptions, async (host, ct) => /// True if the host answered the ping. private async Task IsReachableAsync(IPAddress ipAddress, int portCount, CancellationToken cancellationToken) { - var pingInfo = await PingProbe.PingAsync(ipAddress, _options.Timeout, _options.Attempts, PingBuffer, - cancellationToken).ConfigureAwait(false); + var pingInfo = await PingProbe.PingAsync(ipAddress, _options.ICMPTimeout, _options.ICMPAttempts, + _options.ICMPBuffer, cancellationToken).ConfigureAwait(false); if (pingInfo.Status == IPStatus.Success) return true; diff --git a/Source/NETworkManager.Models/Network/PortScannerOptions.cs b/Source/NETworkManager.Models/Network/PortScannerOptions.cs index 3aefedf054..961e868fdb 100644 --- a/Source/NETworkManager.Models/Network/PortScannerOptions.cs +++ b/Source/NETworkManager.Models/Network/PortScannerOptions.cs @@ -32,11 +32,27 @@ public record PortScannerOptions public required int Attempts { get; init; } /// - /// Ping every host before its ports are scanned (with and ), a host - /// that doesn't answer is not scanned. + /// Ping every host before its ports are scanned (with , and + /// ), a host that doesn't answer is not scanned. /// public required bool PingHostsBeforeScan { get; init; } + /// + /// Number of pings before the scan if there was no answer (see ). + /// + public required int ICMPAttempts { get; init; } + + /// + /// Timeout in milliseconds after which a ping before the scan is considered lost (see + /// ). + /// + public required int ICMPTimeout { get; init; } + + /// + /// Data sent with the ping before the scan (see ). + /// + public required byte[] ICMPBuffer { get; init; } + /// /// Resolve the hostname for an IP address. /// diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index d75f29bdf2..a1356aa9a5 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -167,6 +167,9 @@ public static class GlobalStaticConfiguration public static int PortScanner_Timeout => 2000; public static int PortScanner_Attempts => 1; public static bool PortScanner_PingHostsBeforeScan => false; + public static int PortScanner_ICMPAttempts => 2; + public static int PortScanner_ICMPTimeout => 2000; + public static int PortScanner_ICMPBuffer => 32; public static ExportFileType PortScanner_ExportFileType => ExportFileType.Csv; // Larger scans (hosts x ports) need a confirmation, even larger ones are refused public static long PortScanner_ConfirmPortCount => 65536; diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index a460ab433b..b3892ce80d 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -1554,6 +1554,45 @@ public bool PortScanner_PingHostsBeforeScan } } = GlobalStaticConfiguration.PortScanner_PingHostsBeforeScan; + public int PortScanner_ICMPAttempts + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } = GlobalStaticConfiguration.PortScanner_ICMPAttempts; + + public int PortScanner_ICMPTimeout + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } = GlobalStaticConfiguration.PortScanner_ICMPTimeout; + + public int PortScanner_ICMPBuffer + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } = GlobalStaticConfiguration.PortScanner_ICMPBuffer; + public bool PortScanner_AutoAdjustThreads { get; diff --git a/Source/NETworkManager/ViewModels/PortScannerSettingsViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerSettingsViewModel.cs index 66b7e11f67..65221ec213 100644 --- a/Source/NETworkManager/ViewModels/PortScannerSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortScannerSettingsViewModel.cs @@ -156,6 +156,63 @@ public bool PingHostsBeforeScan } } + /// + /// Gets or sets the number of ICMP attempts of the ping before the scan. + /// + public int ICMPAttempts + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PortScanner_ICMPAttempts = value; + + field = value; + OnPropertyChanged(); + } + } + + /// + /// Gets or sets the ICMP timeout in milliseconds of the ping before the scan. + /// + public int ICMPTimeout + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PortScanner_ICMPTimeout = value; + + field = value; + OnPropertyChanged(); + } + } + + /// + /// Gets or sets the ICMP buffer size of the ping before the scan. + /// + public int ICMPBuffer + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PortScanner_ICMPBuffer = value; + + field = value; + OnPropertyChanged(); + } + } + /// /// Gets or sets whether the number of hosts and ports scanned at the same time is adjusted to the scan (instead of /// and ). @@ -259,6 +316,9 @@ private void LoadSettings() Timeout = SettingsManager.Current.PortScanner_Timeout; Attempts = SettingsManager.Current.PortScanner_Attempts; PingHostsBeforeScan = SettingsManager.Current.PortScanner_PingHostsBeforeScan; + ICMPAttempts = SettingsManager.Current.PortScanner_ICMPAttempts; + ICMPTimeout = SettingsManager.Current.PortScanner_ICMPTimeout; + ICMPBuffer = SettingsManager.Current.PortScanner_ICMPBuffer; ResolveHostname = SettingsManager.Current.PortScanner_ResolveHostname; AutoAdjustThreads = SettingsManager.Current.PortScanner_AutoAdjustThreads; MaxHostThreads = SettingsManager.Current.PortScanner_MaxHostThreads; diff --git a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs index 8842aed6a3..cf4639e7f4 100644 --- a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs @@ -1283,6 +1283,11 @@ private static PortScannerOptions CreateScanOptions() Timeout = SettingsManager.Current.PortScanner_Timeout, Attempts = SettingsManager.Current.PortScanner_Attempts, PingHostsBeforeScan = SettingsManager.Current.PortScanner_PingHostsBeforeScan, + ICMPAttempts = SettingsManager.Current.PortScanner_ICMPAttempts, + ICMPTimeout = SettingsManager.Current.PortScanner_ICMPTimeout, + // Limits of the input - a value edited in the settings file would throw (negative) or make every ping fail + // (.NET's Ping throws for more than 65500 bytes) + ICMPBuffer = new byte[Math.Clamp(SettingsManager.Current.PortScanner_ICMPBuffer, 1, 65500)], ResolveHostname = SettingsManager.Current.PortScanner_ResolveHostname, ShowAllResults = SettingsManager.Current.PortScanner_ShowAllResults }; diff --git a/Source/NETworkManager/Views/PortScannerSettingsView.xaml b/Source/NETworkManager/Views/PortScannerSettingsView.xaml index b0edaf839c..d2407119b9 100644 --- a/Source/NETworkManager/Views/PortScannerSettingsView.xaml +++ b/Source/NETworkManager/Views/PortScannerSettingsView.xaml @@ -118,7 +118,7 @@ Margin="0,0,0,10" /> - + + + + + + + + + + diff --git a/Website/docs/application/port-scanner.md b/Website/docs/application/port-scanner.md index 0e8e0c5022..0837df47c8 100644 --- a/Website/docs/application/port-scanner.md +++ b/Website/docs/application/port-scanner.md @@ -235,7 +235,7 @@ In a local network (LAN), `1` is enough. On an unreliable network (e.g. Wi-Fi, V ### Ping hosts before the scan -Pings every host before its ports are scanned (ICMP echo, with the [Timeout](#timeout-ms) and [Attempts](#attempts) of the port scan). Only hosts that answer are scanned, the number of hosts that were not scanned is shown below the input after the scan. Faster for ranges with many unused IP addresses (e.g. a `/24` with `1-1024`), because the ports of unused addresses are not scanned until their timeout. With [Adjust threads automatically](#adjust-threads-automatically), up to `128` hosts are pinged at the same time and a host is scanned as soon as it answers; otherwise each host is pinged when it is scanned, so [Max. concurrent host threads](#max-concurrent-host-threads) also limits the pings. +Pings every host before its ports are scanned (ICMP echo, with the [Attempts](#attempts-1), [Timeout](#timeout-ms-1) and [Buffer](#buffer) below). Only hosts that answer are scanned, the number of hosts that were not scanned is shown below the input after the scan. Faster for ranges with many unused IP addresses (e.g. a `/24` with `1-1024`), because the ports of unused addresses are not scanned until their timeout. With [Adjust threads automatically](#adjust-threads-automatically), up to `128` hosts are pinged at the same time and a host is scanned as soon as it answers; otherwise each host is pinged when it is scanned, so [Max. concurrent host threads](#max-concurrent-host-threads) also limits the pings. **Type:** `Boolean` @@ -247,6 +247,30 @@ Hosts that block ICMP (e.g. Windows with the firewall enabled) are not scanned, ::: +### Attempts + +Maximum number of ICMP requests sent to each host before the scan (only with [Ping hosts before the scan](#ping-hosts-before-the-scan)). Another request is only sent if the previous one has timed out (e.g. a lost packet), other replies like "destination host unreachable" are final. + +**Type:** `Integer` [Min `1`, Max `10`] + +**Default:** `2` + +### Timeout (ms) + +Timeout in milliseconds for each ICMP request before the scan (only with [Ping hosts before the scan](#ping-hosts-before-the-scan)), after which the packet is considered lost. A host that doesn't answer is skipped after about attempts × timeout. + +**Type:** `Integer` [Min `100`, Max `15000`] + +**Default:** `2000` + +### Buffer + +Size of the buffer for each ICMP request before the scan in bytes (only with [Ping hosts before the scan](#ping-hosts-before-the-scan)). + +**Type:** `Integer` [Min `1`, Max `65500`] + +**Default:** `32` + ### Resolve hostname Resolve the hostname for given IP addresses. diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 853b7b7bbc..6362ba9077 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -110,7 +110,7 @@ Release date: **xx.xx.2026** - Several port profiles can be selected in the settings and deleted at once (context menu or `Del`), the confirmation lists them. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - Added a search box above the result to filter the ports by IP address, hostname, port, protocol, service, description and status, also while the scan is running. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - **Copy** > **IP address** / **Hostname** / **Port** / **IP address:Port** is available if multiple results are selected (one value per line, in the order shown). [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) -- New setting **Ping hosts before the scan** (default off): only hosts that answer a ping are scanned, the number of skipped hosts is shown after the scan. Faster for ranges with many unused IP addresses; hosts that block ICMP are skipped. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) +- New setting **Ping hosts before the scan** (default off): only hosts that answer a ping are scanned, the number of skipped hosts is shown after the scan. The ping has its own **Attempts** (default `2`), **Timeout** (default `2000` ms) and **Buffer** (default `32` bytes) settings, like the IP Scanner. Faster for ranges with many unused IP addresses; hosts that block ICMP are skipped. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - New setting **Adjust threads automatically** (default on): the number of hosts and ports scanned at the same time is adjusted to the scan (up to `512` connections, at most `128` hosts and `128` ports per host). A scan with few ports is much faster, e.g. a `/24` with one port (e.g. `3389`) takes about 4 seconds instead of about 2 minutes; `1-1024` is unchanged. **Max. concurrent host threads** (now up to `256`) and **Max. concurrent port threads** are used if it is disabled, with at most `8,192` connections at the same time. It is disabled when updating if the threads were changed. The hostname (DNS `PTR`) of a host is resolved when its first port is scanned, so the lookups are spread over time like the ports. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - Added a new **Well-known ports** (`1-1024`) default port profile. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564) - The export runs in the background, so the window stays responsive while a large result is exported, and uses the order shown in the result (sorting, grouping) instead of the order in which the ports were scanned. [#3653](https://github.com/BornToBeRoot/NETworkManager/pull/3653) From 4d1c2851a410f0d15bfeff85aed6a25b0ef044dd Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:11:23 +0100 Subject: [PATCH 05/13] Fix: Group dialog: Feature names shown without spaces The group views were only translated with ProfileGroup_* keys (only General / Application exist), so e.g. "RemoteDesktop" was shown. Fall back to the application names like the profile dialog. Co-Authored-By: Claude Opus 5.5 --- .../ProfileGroupToTranslatedStringConverter.cs | 3 ++- Website/docs/changelog/next-release.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/NETworkManager.Converters/ProfileGroupToTranslatedStringConverter.cs b/Source/NETworkManager.Converters/ProfileGroupToTranslatedStringConverter.cs index 63b684a9cf..91fe342144 100644 --- a/Source/NETworkManager.Converters/ProfileGroupToTranslatedStringConverter.cs +++ b/Source/NETworkManager.Converters/ProfileGroupToTranslatedStringConverter.cs @@ -23,7 +23,8 @@ public object Convert(object value, Type targetType, object parameter, CultureIn { return value is not GroupViewName name ? "-/-" - : ResourceTranslator.Translate(ResourceIdentifier.ProfileGroup, name); + : ResourceTranslator.Translate(new[] { ResourceIdentifier.ProfileGroup, ResourceIdentifier.ApplicationName }, + name); } /// diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 6362ba9077..45d12217ea 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -344,6 +344,7 @@ Release date: **xx.xx.2026** - Fixed `Enter` in the search box of the import result importing the profiles (default button). It filters the found profiles right away now, instead of after the delay while typing. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - Fixed columns that couldn't be made wider than the window (by dragging or double-clicking the gripper) in the groups (Settings > Profiles) and the import result. The grid scrolls horizontally now, the last (or main) column still fills the remaining width. [#3557](https://github.com/BornToBeRoot/NETworkManager/issues/3557) +- Fixed the names of the features in the group dialog shown without spaces (e.g. `RemoteDesktop` instead of **Remote Desktop**). **SNMP** From 925e485665f593870afa49c18546bb11a9da48f3 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:15:25 +0100 Subject: [PATCH 06/13] Feature: Port Scanner: Ports in groups Groups get Ports (ports, port ranges and port profiles) for the Port Scanner, used by the profiles of the group that don't override them. Profiles get a Ports checkbox under "Override default settings", like the other settings that can be set in a group. The ports of a profile are resolved in one place (Profiles.Application.PortScanner.GetPorts). Profile file schema version 2 (first use of the version): profiles of older files with ports get the override enabled when loaded, so they keep their ports. Co-Authored-By: Claude Opus 5.5 --- .../Resources/Strings.Designer.cs | 2 +- .../Resources/Strings.resx | 2 +- .../Application/PortScanner.cs | 41 +++++++++ Source/NETworkManager.Profiles/GroupInfo.cs | 18 ++++ .../GroupViewManager.cs | 1 + .../NETworkManager.Profiles/GroupViewName.cs | 1 + .../ProfileFileData.cs | 16 +++- Source/NETworkManager.Profiles/ProfileInfo.cs | 6 ++ .../NETworkManager.Profiles/ProfileManager.cs | 29 +++++++ Source/NETworkManager/ProfileDialogManager.cs | 10 +++ .../ViewModels/GroupViewModel.cs | 52 ++++++++++++ .../ViewModels/PortScannerHostViewModel.cs | 24 ++---- .../ViewModels/PortScannerViewModel.cs | 1 + .../ViewModels/ProfileViewModel.cs | 14 ++++ .../Views/GroupChildWindow.xaml | 83 +++++++++++++++++++ .../Views/ProfileChildWindow.xaml | 23 ++++- Website/docs/application/port-scanner.md | 17 +++- Website/docs/changelog/next-release.md | 1 + 18 files changed, 313 insertions(+), 28 deletions(-) create mode 100644 Source/NETworkManager.Profiles/Application/PortScanner.cs diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 6b5f1c9412..5f055a2d05 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -9239,7 +9239,7 @@ public static string PortProfile { } /// - /// Looks up a localized string similar to A port profile of this profile was not found, the ports saved with the profile are used. Port profiles are saved in the settings and the profile refers to them by an internal ID - if the profile file was shared from another computer, create the port profile and then select it in the profile again (edit the profile).. + /// Looks up a localized string similar to A port profile of this profile or its group was not found, the ports saved with the profile or group are used. Port profiles are saved in the settings and the profile or group refers to them by an internal ID - if the profile file was shared from another computer, create the port profile and then select it in the profile or group again (edit it).. /// public static string PortProfileOfProfileNotFoundMessage { get { diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 5c83137090..f43deb5e46 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -5123,7 +5123,7 @@ Do you want to start the scan? The name must not contain ";" and must not be a port or port range. - A port profile of this profile was not found, the ports saved with the profile are used. Port profiles are saved in the settings and the profile refers to them by an internal ID - if the profile file was shared from another computer, create the port profile and then select it in the profile again (edit the profile). + A port profile of this profile or its group was not found, the ports saved with the profile or group are used. Port profiles are saved in the settings and the profile or group refers to them by an internal ID - if the profile file was shared from another computer, create the port profile and then select it in the profile or group again (edit it). Ping hosts before the scan diff --git a/Source/NETworkManager.Profiles/Application/PortScanner.cs b/Source/NETworkManager.Profiles/Application/PortScanner.cs new file mode 100644 index 0000000000..2aa239b111 --- /dev/null +++ b/Source/NETworkManager.Profiles/Application/PortScanner.cs @@ -0,0 +1,41 @@ +using NETworkManager.Models.Network; +using NETworkManager.Settings; + +namespace NETworkManager.Profiles.Application; + +public static class PortScanner +{ + /// + /// Gets the ports to scan for a profile: its own ports (if overridden), otherwise the ports of its group (if + /// overridden), otherwise none. Port profiles with their current name (also after a rename); if a port profile + /// can't be found (deleted, or a profile file shared from another computer - port profiles are saved in the + /// settings), the ports saved with the profile or group. + /// + /// Profile to get the ports for. + /// True if a port profile can't be found. + /// Ports to scan, empty if neither the profile nor its group has ports. + public static string GetPorts(ProfileInfo profile, out bool portProfileNotFound) + { + // Get group info + var group = ProfileManager.GetGroupByName(profile.Group); + + var (ports, portsInput) = profile.PortScanner_OverridePorts + ? (profile.PortScanner_Ports, profile.PortScanner_PortsInput) + : group.PortScanner_OverridePorts + ? (group.PortScanner_Ports, group.PortScanner_PortsInput) + : (string.Empty, null); + + portProfileNotFound = false; + + if (string.IsNullOrEmpty(portsInput)) + return ports; + + if (PortProfile.TryDecodeReferences(portsInput, SettingsManager.Current.PortScanner_PortProfiles, + out var decodedPorts)) + return decodedPorts; + + portProfileNotFound = true; + + return ports; + } +} diff --git a/Source/NETworkManager.Profiles/GroupInfo.cs b/Source/NETworkManager.Profiles/GroupInfo.cs index 4842bcccc2..7fb4fb4e74 100644 --- a/Source/NETworkManager.Profiles/GroupInfo.cs +++ b/Source/NETworkManager.Profiles/GroupInfo.cs @@ -41,6 +41,11 @@ public GroupInfo(GroupInfo group) : this(group.Name) // Profiles Profiles = group.Profiles; + // Port Scanner + PortScanner_OverridePorts = group.PortScanner_OverridePorts; + PortScanner_Ports = group.PortScanner_Ports; + PortScanner_PortsInput = group.PortScanner_PortsInput; + // Remote Desktop RemoteDesktop_UseCredentials = group.RemoteDesktop_UseCredentials; RemoteDesktop_Username = group.RemoteDesktop_Username; @@ -175,6 +180,19 @@ public GroupInfo(GroupInfo group) : this(group.Name) [XmlIgnore] public List Profiles { get; set; } + public bool PortScanner_OverridePorts { get; set; } + + /// + /// Ports to scan (ports and port ranges) for the profiles of the group, see + /// . + /// + public string PortScanner_Ports { get; set; } + + /// + /// Ports as entered, if they contain port profiles, see . + /// + public string PortScanner_PortsInput { get; set; } + public bool RemoteDesktop_UseCredentials { get; set; } public string RemoteDesktop_Username { get; set; } public string RemoteDesktop_Domain { get; set; } diff --git a/Source/NETworkManager.Profiles/GroupViewManager.cs b/Source/NETworkManager.Profiles/GroupViewManager.cs index 4d3a910e75..8b255b41c5 100644 --- a/Source/NETworkManager.Profiles/GroupViewManager.cs +++ b/Source/NETworkManager.Profiles/GroupViewManager.cs @@ -13,6 +13,7 @@ public static class GroupViewManager new GroupViewInfo(GroupViewName.General, new PackIconModern { Kind = PackIconModernKind.Box }), // Applications + new GroupViewInfo(GroupViewName.PortScanner, ApplicationManager.GetIcon(ApplicationName.PortScanner)), new GroupViewInfo(GroupViewName.RemoteDesktop, ApplicationManager.GetIcon(ApplicationName.RemoteDesktop)), new GroupViewInfo(GroupViewName.PowerShell, ApplicationManager.GetIcon(ApplicationName.PowerShell)), new GroupViewInfo(GroupViewName.PuTTY, ApplicationManager.GetIcon(ApplicationName.PuTTY)), diff --git a/Source/NETworkManager.Profiles/GroupViewName.cs b/Source/NETworkManager.Profiles/GroupViewName.cs index b36d3bffe6..b6929f572b 100644 --- a/Source/NETworkManager.Profiles/GroupViewName.cs +++ b/Source/NETworkManager.Profiles/GroupViewName.cs @@ -3,6 +3,7 @@ public enum GroupViewName { General, + PortScanner, RemoteDesktop, PowerShell, PuTTY, diff --git a/Source/NETworkManager.Profiles/ProfileFileData.cs b/Source/NETworkManager.Profiles/ProfileFileData.cs index 989a642627..bfbe95a084 100644 --- a/Source/NETworkManager.Profiles/ProfileFileData.cs +++ b/Source/NETworkManager.Profiles/ProfileFileData.cs @@ -41,7 +41,19 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = null) public bool ProfilesChanged { get; set; } /// - /// Schema version for handling future migrations. + /// Current schema version of the profile file (see ). Version history: + /// + /// 1: Initial version. + /// + /// 2: Port Scanner ports can be inherited from the group, a profile only uses its own ports if + /// is set. + /// + /// + /// + public const int CurrentVersion = 2; + + /// + /// Schema version for handling migrations (see ). /// public int Version { @@ -54,7 +66,7 @@ public int Version field = value; OnPropertyChanged(); } - } = 1; + } = CurrentVersion; /// /// Date of the last backup (used for daily backup tracking). diff --git a/Source/NETworkManager.Profiles/ProfileInfo.cs b/Source/NETworkManager.Profiles/ProfileInfo.cs index ade5fa6376..18216d8cda 100644 --- a/Source/NETworkManager.Profiles/ProfileInfo.cs +++ b/Source/NETworkManager.Profiles/ProfileInfo.cs @@ -62,6 +62,7 @@ public ProfileInfo(ProfileInfo profile) PortScanner_Enabled = profile.PortScanner_Enabled; PortScanner_InheritHost = profile.PortScanner_InheritHost; PortScanner_Host = profile.PortScanner_Host; + PortScanner_OverridePorts = profile.PortScanner_OverridePorts; PortScanner_Ports = profile.PortScanner_Ports; PortScanner_PortsInput = profile.PortScanner_PortsInput; @@ -315,6 +316,11 @@ public ProfileInfo(ProfileInfo profile) public bool PortScanner_InheritHost { get; set; } = true; public string PortScanner_Host { get; set; } + /// + /// Use of the profile instead of the ports of the group. + /// + public bool PortScanner_OverridePorts { get; set; } + /// /// Ports to scan (ports and port ranges). If port profiles were entered, their ports at the time the profile was /// saved - used if a port profile of can't be found (e.g. a profile file diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 38ed36f408..c4bb3ad038 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -1014,6 +1014,7 @@ private static void DeserializeFromJson(string jsonString) if (profileFileData != null) { LoadedProfileFileData = profileFileData; + UpgradeLoadedProfileFileData(); return; } } @@ -1031,9 +1032,12 @@ private static void DeserializeFromJson(string jsonString) // Create ProfileFileData wrapper for legacy format LoadedProfileFileData = new ProfileFileData { + Version = 1, GroupsSerializable = groupsSerializable }; + UpgradeLoadedProfileFileData(); + Log.Info("Successfully loaded profile file in legacy format. It will be migrated to new format on next save."); } @@ -1053,8 +1057,33 @@ private static void DeserializeFromXmlStream(Stream stream) LoadedProfileFileData = new ProfileFileData { + Version = 1, GroupsSerializable = groupsSerializable }; + + UpgradeLoadedProfileFileData(); + } + + /// + /// Migrates the loaded profile data from an older schema version to + /// . The file is updated the next time it is saved. + /// + private static void UpgradeLoadedProfileFileData() + { + if (LoadedProfileFileData.Version >= ProfileFileData.CurrentVersion) + return; + + Log.Info($"Upgrade profile file data from version {LoadedProfileFileData.Version} to {ProfileFileData.CurrentVersion}..."); + + // Version 2: Port Scanner ports can be inherited from the group - profiles keep their own ports + if (LoadedProfileFileData.Version < 2) + { + foreach (var profile in LoadedProfileFileData.Groups.SelectMany(x => x.Profiles) + .Where(x => !string.IsNullOrEmpty(x.PortScanner_Ports))) + profile.PortScanner_OverridePorts = true; + } + + LoadedProfileFileData.Version = ProfileFileData.CurrentVersion; } /// diff --git a/Source/NETworkManager/ProfileDialogManager.cs b/Source/NETworkManager/ProfileDialogManager.cs index 2c62478873..602fef75fb 100644 --- a/Source/NETworkManager/ProfileDialogManager.cs +++ b/Source/NETworkManager/ProfileDialogManager.cs @@ -55,6 +55,7 @@ private static ProfileInfo ParseProfileInfo(ProfileViewModel instance) PortScanner_Host = instance.PortScanner_InheritHost ? instance.Host?.Trim() : instance.PortScanner_Host?.Trim(), + PortScanner_OverridePorts = instance.PortScanner_OverridePorts, // Port profiles are referenced by their ID (found after a rename), their ports are saved as well (used if // a port profile can't be found, e.g. a profile file from another computer) PortScanner_Ports = PortProfile.TryResolvePorts(instance.PortScanner_Ports, @@ -321,6 +322,15 @@ private static GroupInfo ParseGroupInfo(GroupViewModel instance) Profiles = profiles, + // Port Scanner - like the profile (see ParseProfileInfo) + PortScanner_OverridePorts = instance.PortScanner_OverridePorts, + PortScanner_Ports = PortProfile.TryResolvePorts(instance.PortScanner_Ports, + SettingsManager.Current.PortScanner_PortProfiles, out var portScannerPorts) + ? portScannerPorts + : instance.PortScanner_Ports?.Trim(), + PortScanner_PortsInput = PortProfile.EncodeReferences(instance.PortScanner_Ports, + SettingsManager.Current.PortScanner_PortProfiles), + // Remote Desktop RemoteDesktop_UseCredentials = instance.RemoteDesktop_UseCredentials, RemoteDesktop_Username = diff --git a/Source/NETworkManager/ViewModels/GroupViewModel.cs b/Source/NETworkManager/ViewModels/GroupViewModel.cs index a3debe5d79..be9f804a6b 100644 --- a/Source/NETworkManager/ViewModels/GroupViewModel.cs +++ b/Source/NETworkManager/ViewModels/GroupViewModel.cs @@ -3,6 +3,7 @@ using NETworkManager.Models.PuTTY; using NETworkManager.Models.RemoteDesktop; using NETworkManager.Profiles; +using NETworkManager.Settings; using NETworkManager.Utilities; using System; using System.Collections.Generic; @@ -107,6 +108,48 @@ public string Description #endregion + #region Port Scanner + + public bool PortScanner_OverridePorts + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + + private readonly InputSuggestions _portScannerPortsSuggestions = InputSuggestions.ForPortScannerPorts(); + + /// + /// Gets the port profiles and the ports history of the Port Scanner for the ports input (grouped). + /// + public ICollectionView PortScanner_PortsSuggestions => _portScannerPortsSuggestions.View; + + /// + /// Gets the names of the port profiles for the Tab completion of the ports input of the Port Scanner. + /// + public IEnumerable PortScanner_PortProfileNames => _portScannerPortsSuggestions.ProfileNames; + + public string PortScanner_Ports + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + + #endregion + #region Remote Desktop /// @@ -1590,6 +1633,15 @@ public GroupViewModel(Action saveCommand, Action Name = groupInfo.Name; Description = groupInfo.Description; + // Port Scanner + PortScanner_OverridePorts = groupInfo.PortScanner_OverridePorts; + // Port profiles with their current name (also after a rename). A port profile that can't be found (deleted, or + // a profile file from another computer): the ports saved with the group. + PortScanner_Ports = PortProfile.TryDecodeReferences(groupInfo.PortScanner_PortsInput, + SettingsManager.Current.PortScanner_PortProfiles, out var portScannerPortsInput) + ? portScannerPortsInput + : groupInfo.PortScanner_Ports; + // Remote Desktop RemoteDesktop_UseCredentials = groupInfo.RemoteDesktop_UseCredentials; RemoteDesktop_Username = groupInfo.RemoteDesktop_Username; diff --git a/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs index e095857c2c..2c881f7a51 100644 --- a/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs @@ -2,9 +2,7 @@ using NETworkManager.Controls; using NETworkManager.Localization.Resources; using NETworkManager.Models; -using NETworkManager.Models.Network; using NETworkManager.Profiles; -using NETworkManager.Settings; using NETworkManager.Utilities; using NETworkManager.Views; using System; @@ -152,23 +150,11 @@ public void AddTab(string host = null, string ports = null, bool start = true, s public void AddTab(ProfileInfo profile, bool start = true) { - // Port profiles with their current name and ports. A port profile that can't be found (deleted, or a profile - // file shared from another computer - port profiles are saved in the settings): the ports saved with the - // profile, with a note. - if (string.IsNullOrEmpty(profile.PortScanner_PortsInput)) - { - AddTab(profile.PortScanner_Host, profile.PortScanner_Ports, start); - } - else if (PortProfile.TryDecodeReferences(profile.PortScanner_PortsInput, - SettingsManager.Current.PortScanner_PortProfiles, out var ports)) - { - AddTab(profile.PortScanner_Host, ports, start); - } - else - { - AddTab(profile.PortScanner_Host, profile.PortScanner_Ports, start, - Strings.PortProfileOfProfileNotFoundMessage); - } + // Ports of the profile or its group. A port profile that can't be found: the saved ports, with a note. + var ports = NETworkManager.Profiles.Application.PortScanner.GetPorts(profile, out var portProfileNotFound); + + AddTab(profile.PortScanner_Host, ports, start, + portProfileNotFound ? Strings.PortProfileOfProfileNotFoundMessage : null); } #endregion diff --git a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs index cf4639e7f4..34b689fa31 100644 --- a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs @@ -737,6 +737,7 @@ private async void AddProfileSelectedHostAction(PortScannerPortInfo row) Host = row.IPAddress.ToString(), // Additional data + PortScanner_OverridePorts = true, PortScanner_Ports = _lastPortsInput }; diff --git a/Source/NETworkManager/ViewModels/ProfileViewModel.cs b/Source/NETworkManager/ViewModels/ProfileViewModel.cs index ca4c44aced..0de47cc35e 100644 --- a/Source/NETworkManager/ViewModels/ProfileViewModel.cs +++ b/Source/NETworkManager/ViewModels/ProfileViewModel.cs @@ -89,6 +89,7 @@ public ProfileViewModel(Action saveCommand, Action diff --git a/Source/NETworkManager/Views/GroupChildWindow.xaml b/Source/NETworkManager/Views/GroupChildWindow.xaml index 38676ab0b2..9517ceb1cb 100644 --- a/Source/NETworkManager/Views/GroupChildWindow.xaml +++ b/Source/NETworkManager/Views/GroupChildWindow.xaml @@ -11,6 +11,7 @@ xmlns:network="clr-namespace:NETworkManager.Models.Network;assembly=NETworkManager.Models" xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters" xmlns:validators="clr-namespace:NETworkManager.Validators;assembly=NETworkManager.Validators" + xmlns:controls="clr-namespace:NETworkManager.Controls;assembly=NETworkManager.Controls" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" xmlns:viewModels="clr-namespace:NETworkManager.ViewModels" xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF" @@ -148,6 +149,23 @@ + + + + + + + + + + + @@ -631,6 +649,59 @@ + + + + + + - + + + - - + @@ -4378,6 +4392,7 @@ + diff --git a/Website/docs/application/port-scanner.md b/Website/docs/application/port-scanner.md index 0837df47c8..8e6f65eb78 100644 --- a/Website/docs/application/port-scanner.md +++ b/Website/docs/application/port-scanner.md @@ -140,7 +140,7 @@ See also the [Port Scanner](./port-scanner) example inputs for more information ### Ports -TCP ports to scan each host for: ports, port ranges and names of [port profiles](#port-profiles) (selectable in the list). +TCP ports to scan each host for: ports, port ranges and names of [port profiles](#port-profiles) (selectable in the list). Only used if the checkbox is enabled (then the ports are required), otherwise the [ports of the group](#ports-2) are used (if set there). **Type:** `String` @@ -164,6 +164,21 @@ See also the [Port Scanner](./port-scanner) example inputs for more information ::: +## Group + +### Ports + +TCP ports to scan for the profiles of the group that don't set their own [ports](#ports-1) (required if the checkbox is enabled), like the ports of a profile: ports, port ranges and names of [port profiles](#port-profiles). If neither the profile nor the group has ports, the ports input is empty and the scan is not started. + +**Type:** `String` + +**Default:** `Empty` + +**Example:** + +- `22; 3389; 5985` +- `Webserver; 8080` + ## Settings ### Port profiles diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 45d12217ea..0515d0c9cb 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -113,6 +113,7 @@ Release date: **xx.xx.2026** - New setting **Ping hosts before the scan** (default off): only hosts that answer a ping are scanned, the number of skipped hosts is shown after the scan. The ping has its own **Attempts** (default `2`), **Timeout** (default `2000` ms) and **Buffer** (default `32` bytes) settings, like the IP Scanner. Faster for ranges with many unused IP addresses; hosts that block ICMP are skipped. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - New setting **Adjust threads automatically** (default on): the number of hosts and ports scanned at the same time is adjusted to the scan (up to `512` connections, at most `128` hosts and `128` ports per host). A scan with few ports is much faster, e.g. a `/24` with one port (e.g. `3389`) takes about 4 seconds instead of about 2 minutes; `1-1024` is unchanged. **Max. concurrent host threads** (now up to `256`) and **Max. concurrent port threads** are used if it is disabled, with at most `8,192` connections at the same time. It is disabled when updating if the threads were changed. The hostname (DNS `PTR`) of a host is resolved when its first port is scanned, so the lookups are spread over time like the ports. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - Added a new **Well-known ports** (`1-1024`) default port profile. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564) +- Groups: new **Ports** setting (ports, port ranges and port profiles) for the Port Scanner, used by the profiles of the group that don't override them (new **Ports** checkbox in the profile, like the other settings that can be set in the group). Existing profiles with ports keep them (the checkbox is enabled when the profile file is loaded). - The export runs in the background, so the window stays responsive while a large result is exported, and uses the order shown in the result (sorting, grouping) instead of the order in which the ports were scanned. [#3653](https://github.com/BornToBeRoot/NETworkManager/pull/3653) - Ports are started one after another (every 1 ms, shorter with short timeouts and many threads) instead of all at the same moment. This avoids bursts of hundreds of connection requests within a few milliseconds and makes the progress smoother. [#3653](https://github.com/BornToBeRoot/NETworkManager/pull/3653) From c6d53be5a1c694aad973376090bb30e5b940763a Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:46:04 +0100 Subject: [PATCH 07/13] Fix: Firewall: Feature name not translatable ApplicationName_Firewall was missing, so the name fell back to the enum name wherever application names are translated (list of features, settings, run command). Co-Authored-By: Claude Opus 5.5 --- .../Resources/Strings.Designer.cs | 9 +++++++++ .../NETworkManager.Localization/Resources/Strings.resx | 3 +++ Website/docs/changelog/next-release.md | 1 + 3 files changed, 13 insertions(+) diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 5f055a2d05..ec17d2544f 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -951,6 +951,15 @@ public static string ApplicationName_DNSLookup { } } + /// + /// Looks up a localized string similar to Firewall. + /// + public static string ApplicationName_Firewall { + get { + return ResourceManager.GetString("ApplicationName_Firewall", resourceCulture); + } + } + /// /// Looks up a localized string similar to Hosts File Editor. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index f43deb5e46..437cc8290c 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -5143,4 +5143,7 @@ Do you want to start the scan? A DNS server of this profile was not found, the DNS servers saved with the profile are used. DNS servers are saved in the settings and the profile refers to them by an internal ID - if the profile file was shared from another computer, create the DNS server and then select it in the profile again (edit the profile). + + Firewall + \ No newline at end of file diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 0515d0c9cb..4f5b90481f 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -192,6 +192,7 @@ Release date: **xx.xx.2026** **Firewall** +- Fixed the name of the feature not being translatable in the list of features, the settings and the run command (missing translation key). - Fixed `F5` not refreshing the rule list, although it is documented as shortcut. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637) - Fixed an empty rule list with "Reloaded at ..." being shown if the rules could not be loaded (e.g. the Windows Firewall service is not running). An error is shown now, and a message if the rules could only be loaded partially. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637) - Fixed the Firewall not working until the application was restarted if loading the PowerShell module `NetSecurity` failed once. [#3637](https://github.com/BornToBeRoot/NETworkManager/pull/3637) From b269b7f6a7988a6af3ee9eb679505ec8b0be05e0 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:14:45 +0100 Subject: [PATCH 08/13] Fix: DNS Lookup: Clear message for PTR with a hostname A hostname looked up with PTR showed the exception text "An invalid IP address was specified". Hostnames are skipped with a message now, the other hosts are looked up. The IP check of the DNS Lookup (PTR for IP addresses) moves to IPAddressHelper to be shared. Co-Authored-By: Claude Opus 5.5 --- .../Resources/Strings.Designer.cs | 9 +++++ .../Resources/Strings.resx | 3 ++ .../Utilities/IPAddressHelperTests.cs | 36 +++++++++++++++++++ .../IPAddressHelper.cs | 29 +++++++++++++++ .../ViewModels/DNSLookupHostViewModel.cs | 16 +-------- .../ViewModels/DNSLookupViewModel.cs | 23 +++++++++++- Website/docs/application/dns-lookup.md | 6 ++++ Website/docs/changelog/next-release.md | 1 + 8 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 Source/NETworkManager.Tests/Utilities/IPAddressHelperTests.cs diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index ec17d2544f..cbb794813b 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -9662,6 +9662,15 @@ public static string PTRRecord { } } + /// + /// Looks up a localized string similar to PTR records can only be looked up for IP addresses, "{0}" is skipped.. + /// + public static string PTRRequiresIPAddressX { + get { + return ResourceManager.GetString("PTRRequiresIPAddressX", resourceCulture); + } + } + /// /// Looks up a localized string similar to Public. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 437cc8290c..5b0837ae77 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -5146,4 +5146,7 @@ Do you want to start the scan? Firewall + + PTR records can only be looked up for IP addresses, "{0}" is skipped. + \ No newline at end of file diff --git a/Source/NETworkManager.Tests/Utilities/IPAddressHelperTests.cs b/Source/NETworkManager.Tests/Utilities/IPAddressHelperTests.cs new file mode 100644 index 0000000000..a3467d7e0b --- /dev/null +++ b/Source/NETworkManager.Tests/Utilities/IPAddressHelperTests.cs @@ -0,0 +1,36 @@ +using NETworkManager.Utilities; +using Xunit; + +namespace NETworkManager.Tests.Utilities; + +public class IPAddressHelperTests +{ + [Theory] + [InlineData("10.0.0.1", true)] + [InlineData(" 10.0.0.1 ", true)] + [InlineData("2001:db8::1", true)] + [InlineData("::1", true)] + [InlineData("10", false)] + [InlineData("127.1", false)] + [InlineData("server-01.borntoberoot.net", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void IsIPAddress(string host, bool expected) + { + Assert.Equal(expected, IPAddressHelper.IsIPAddress(host)); + } + + [Theory] + [InlineData("10.0.0.1", true)] + [InlineData("10.0.0.1; 2001:db8::1", true)] + [InlineData("10.0.0.1; 10.0.0.2;", true)] + [InlineData("10.0.0.1;; 10.0.0.2", true)] + [InlineData("10.0.0.1; example.com", false)] + [InlineData(";", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void AreIPAddresses(string hosts, bool expected) + { + Assert.Equal(expected, IPAddressHelper.AreIPAddresses(hosts)); + } +} diff --git a/Source/NETworkManager.Utilities/IPAddressHelper.cs b/Source/NETworkManager.Utilities/IPAddressHelper.cs index 988289f0da..a4bfbdca2f 100644 --- a/Source/NETworkManager.Utilities/IPAddressHelper.cs +++ b/Source/NETworkManager.Utilities/IPAddressHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net; using System.Net.Sockets; @@ -6,6 +7,34 @@ namespace NETworkManager.Utilities; public static class IPAddressHelper { + /// + /// Checks if a host is an IP address: an IPv4 address in the full form (shortened forms like "10" are host + /// names) or an IPv6 address. + /// + /// Host to check. + /// True if the host is an IP address. + public static bool IsIPAddress(string host) + { + return !string.IsNullOrWhiteSpace(host) && + (RegexHelper.IPv4AddressRegex().IsMatch(host.Trim()) || + host.Contains(':') && IPAddress.TryParse(host.Trim(), out var ipAddress) && + ipAddress.AddressFamily == AddressFamily.InterNetworkV6); + } + + /// + /// Checks if all hosts (separated by ";", empty entries like a trailing ";" are ignored) are IP addresses (see + /// ). + /// + /// Hosts to check. + /// True if there is at least one host and all hosts are IP addresses. + public static bool AreIPAddresses(string hosts) + { + var entries = (hosts ?? string.Empty).Split(';', StringSplitOptions.RemoveEmptyEntries | + StringSplitOptions.TrimEntries); + + return entries.Length > 0 && entries.All(IsIPAddress); + } + /// /// Test if an IP address is a private address. /// diff --git a/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs b/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs index 8ed0d47689..980b0538f0 100644 --- a/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs @@ -11,9 +11,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Windows.Input; namespace NETworkManager.ViewModels; @@ -163,7 +160,7 @@ private static void CloseItemAction(ItemActionCallbackArgs args) public void AddTab(string host = null, string dnsServer = null, QueryType? queryType = null, bool start = true, string message = null) { - if (queryType == null && IsIPAddresses(host)) + if (queryType == null && IPAddressHelper.AreIPAddresses(host)) queryType = QueryType.PTR; var tabId = Guid.NewGuid(); @@ -174,17 +171,6 @@ public void AddTab(string host = null, string dnsServer = null, QueryType? query SelectedTabIndex = TabItems.Count - 1; } - /// - /// Checks if the host(s) are only IP addresses (shortened IPv4 forms like "10" are host names). - /// - private static bool IsIPAddresses(string hosts) - { - return !string.IsNullOrWhiteSpace(hosts) && hosts.Split(';').Select(x => x.Trim()).All(x => - RegexHelper.IPv4AddressRegex().IsMatch(x) || - x.Contains(':') && IPAddress.TryParse(x, out var ipAddress) && - ipAddress.AddressFamily == AddressFamily.InterNetworkV6); - } - /// /// Adds a new tab for the host of the specified profile. /// diff --git a/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs b/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs index 26499769c3..1fe380227e 100644 --- a/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs +++ b/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs @@ -394,6 +394,27 @@ private async Task QueryAsync() DragablzTabItem.SetTabHeader(_tabId, Host); + List hosts = [.. Host.Split(';').Select(x => x.Trim())]; + + // PTR needs IP addresses (e.g. from a profile or the run command): hostnames are skipped with a message + // instead of the error of the lookup ("An invalid IP address was specified"). Checked before the DNS servers + // are resolved and the history is changed, so nothing is done if no host is left. Every address the lookup + // can parse is valid (also shortened IPv4 forms like "127.1"). + if (QueryType == QueryType.PTR) + { + foreach (var host in hosts.Where(x => x.Length > 0 && !IPAddress.TryParse(x, out _))) + AppendStatusMessage(string.Format(Strings.PTRRequiresIPAddressX, host)); + + hosts = [.. hosts.Where(x => IPAddress.TryParse(x, out _))]; + + if (hosts.Count == 0) + { + IsRunning = false; + + return; + } + } + ListHelper.Modify(SettingsManager.Current.DNSLookup_HostHistory, Host, SettingsManager.Current.General_HistoryListEntries); @@ -484,7 +505,7 @@ private async Task QueryAsync() dnsLookup.LookupError += DNSLookup_LookupError; dnsLookup.LookupComplete += DNSLookup_LookupComplete; - dnsLookup.ResolveAsync([.. Host.Split(';').Select(x => x.Trim())]); + dnsLookup.ResolveAsync(hosts); } /// diff --git a/Website/docs/application/dns-lookup.md b/Website/docs/application/dns-lookup.md index 34513cb955..2745170bc2 100644 --- a/Website/docs/application/dns-lookup.md +++ b/Website/docs/application/dns-lookup.md @@ -53,6 +53,12 @@ Example: `server-01.borntoberoot.net; 10.0.0.1` ::: +:::note + +`PTR` can only be looked up for IP addresses. Hostnames are skipped with a message. + +::: + The DNS server can be selected from a list of configured servers or you can enter a custom DNS server in the format `|:` (`` is optional, to use a custom port with IPv6 enclose the address in square brackets: `[]:53`). Names of configured DNS servers (case-insensitive) and custom DNS servers can be combined with a semicolon (e.g. `Google Public DNS; Cloudflare; 9.9.9.9`), every DNS server is queried once. `[Windows DNS]` together with other DNS servers adds the DNS servers configured in Windows. The list shows the recently used DNS servers (history, a single configured DNS server is not added) and the configured DNS servers (name, with the IP addresses below). `Tab` completes the name of a configured DNS server that is being typed (e.g. `google; cl` → `google; Cloudflare`), `Tab` again shows the next match. ### Context menu diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 4f5b90481f..69a244d682 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -258,6 +258,7 @@ Release date: **xx.xx.2026** - Fixed a DNS server opened with the run command in another case than configured (e.g. `cloudflare` for `Cloudflare`) being looked up as hostname instead of using the configured DNS server. Names of DNS servers are case-insensitive now. [#3660](https://github.com/BornToBeRoot/NETworkManager/pull/3660) - 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) - Fixed columns that couldn't be made wider than the window (by dragging or double-clicking the gripper) in the result and the DNS server profiles in the settings. The grid scrolls horizontally now, the last (or main) column still fills the remaining width. [#3557](https://github.com/BornToBeRoot/NETworkManager/issues/3557) +- Fixed a technical error (*An invalid IP address was specified*) being shown for a hostname looked up with `PTR`. A message is shown and the hostname is skipped, the other hosts are looked up. **IP Scanner** From ff4ca0aa71eb7c471f2ae1078f9ec235b70a9c95 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:19:03 +0100 Subject: [PATCH 09/13] Feature: DNS Lookup: DNS server in groups, record type in profiles - Groups get DNS server (like the profile), used by the profiles of the group that don't override it: profile -> group -> selected in DNS Lookup. - Profiles get Type (record type). Without it, a profile is looked up with PTR for IP addresses and A for hostnames instead of the record type last selected in DNS Lookup. PTR with a hostname is an error in the profile (can't be saved). - DNS server and type are under "Override default settings" in the profile, like the other settings that can be set in a group. - DNS server and type of a profile are resolved in one place (Profiles.Application.DNSLookup). Co-Authored-By: Claude Opus 5.5 --- .../Resources/Strings.Designer.cs | 11 ++- .../Resources/Strings.resx | 5 +- .../Application/DNSLookup.cs | 62 ++++++++++++++ Source/NETworkManager.Profiles/GroupInfo.cs | 18 ++++ .../GroupViewManager.cs | 1 + .../NETworkManager.Profiles/GroupViewName.cs | 1 + Source/NETworkManager.Profiles/ProfileInfo.cs | 10 +++ Source/NETworkManager/ProfileDialogManager.cs | 9 ++ .../ViewModels/DNSLookupHostViewModel.cs | 28 ++----- .../ViewModels/GroupViewModel.cs | 51 +++++++++++ .../ViewModels/ProfileViewModel.cs | 70 ++++++++++++++++ .../Views/GroupChildWindow.xaml | 84 +++++++++++++++++++ .../Views/ProfileChildWindow.xaml | 62 +++++++++++++- Website/docs/application/dns-lookup.md | 31 ++++++- Website/docs/changelog/next-release.md | 2 +- 15 files changed, 414 insertions(+), 31 deletions(-) create mode 100644 Source/NETworkManager.Profiles/Application/DNSLookup.cs diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index cbb794813b..f4e3005577 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -3695,7 +3695,7 @@ public static string DNSServer { } /// - /// Looks up a localized string similar to A DNS server of this profile was not found, the DNS servers saved with the profile are used. DNS servers are saved in the settings and the profile refers to them by an internal ID - if the profile file was shared from another computer, create the DNS server and then select it in the profile again (edit the profile).. + /// Looks up a localized string similar to A DNS server of this profile or its group was not found, the DNS servers saved with the profile or group are used. DNS servers are saved in the settings and the profile or group refers to them by an internal ID - if the profile file was shared from another computer, create the DNS server and then select it in the profile or group again (edit it).. /// public static string DNSServerOfProfileNotFoundMessage { get { @@ -4706,6 +4706,15 @@ public static string ErrorMessage_PortProfileNameInvalid { } } + /// + /// Looks up a localized string similar to PTR records can only be looked up for IP addresses.. + /// + public static string ErrorMessage_PTRRequiresIPAddresses { + get { + return ResourceManager.GetString("ErrorMessage_PTRRequiresIPAddresses", resourceCulture); + } + } + /// /// Looks up a localized string similar to The settings on this page contain errors. Correct them to be able to save.. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 5b0837ae77..4bee6b1c4b 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -5141,7 +5141,7 @@ Do you want to start the scan? The name must not contain ";". - A DNS server of this profile was not found, the DNS servers saved with the profile are used. DNS servers are saved in the settings and the profile refers to them by an internal ID - if the profile file was shared from another computer, create the DNS server and then select it in the profile again (edit the profile). + A DNS server of this profile or its group was not found, the DNS servers saved with the profile or group are used. DNS servers are saved in the settings and the profile or group refers to them by an internal ID - if the profile file was shared from another computer, create the DNS server and then select it in the profile or group again (edit it). Firewall @@ -5149,4 +5149,7 @@ Do you want to start the scan? PTR records can only be looked up for IP addresses, "{0}" is skipped. + + PTR records can only be looked up for IP addresses. + \ No newline at end of file diff --git a/Source/NETworkManager.Profiles/Application/DNSLookup.cs b/Source/NETworkManager.Profiles/Application/DNSLookup.cs new file mode 100644 index 0000000000..9dc2b73733 --- /dev/null +++ b/Source/NETworkManager.Profiles/Application/DNSLookup.cs @@ -0,0 +1,62 @@ +using DnsClient; +using NETworkManager.Models.Network; +using NETworkManager.Settings; +using NETworkManager.Utilities; + +namespace NETworkManager.Profiles.Application; + +public static class DNSLookup +{ + /// + /// Gets the DNS servers for a profile: its own DNS servers (if overridden), otherwise the DNS servers of its + /// group (if overridden), otherwise none (the DNS server selected in the DNS Lookup is used). DNS servers by name + /// with their current name (also after a rename); if a DNS server can't be found (deleted, or a profile file + /// shared from another computer - DNS servers are saved in the settings), the servers saved with the profile or + /// group. + /// + /// Profile to get the DNS servers for. + /// True if a DNS server can't be found. + /// DNS servers, null if neither the profile nor its group has DNS servers. + public static string GetDNSServer(ProfileInfo profile, out bool dnsServerNotFound) + { + // Get group info + var group = ProfileManager.GetGroupByName(profile.Group); + + var (dnsServer, dnsServerInput) = profile.DNSLookup_OverrideDNSServer + ? (profile.DNSLookup_DNSServer, profile.DNSLookup_DNSServerInput) + : group.DNSLookup_OverrideDNSServer + ? (group.DNSLookup_DNSServer, group.DNSLookup_DNSServerInput) + : (null, null); + + dnsServerNotFound = false; + + if (string.IsNullOrWhiteSpace(dnsServer)) + return null; + + if (string.IsNullOrEmpty(dnsServerInput)) + return dnsServer; + + if (DNSServer.TryDecodeReferences(dnsServerInput, SettingsManager.Current.DNSLookup_DNSServers, + out var decodedDNSServer)) + return decodedDNSServer; + + dnsServerNotFound = true; + + return dnsServer; + } + + /// + /// Gets the query type for a profile: its own type (if overridden), otherwise PTR for IP addresses and A for + /// hostnames. + /// + /// Profile to get the query type for. + /// Query type. + public static QueryType GetQueryType(ProfileInfo profile) + { + return profile.DNSLookup_OverrideQueryType + ? profile.DNSLookup_QueryType + : IPAddressHelper.AreIPAddresses(profile.DNSLookup_Host) + ? QueryType.PTR + : QueryType.A; + } +} diff --git a/Source/NETworkManager.Profiles/GroupInfo.cs b/Source/NETworkManager.Profiles/GroupInfo.cs index 7fb4fb4e74..1f9289df58 100644 --- a/Source/NETworkManager.Profiles/GroupInfo.cs +++ b/Source/NETworkManager.Profiles/GroupInfo.cs @@ -46,6 +46,11 @@ public GroupInfo(GroupInfo group) : this(group.Name) PortScanner_Ports = group.PortScanner_Ports; PortScanner_PortsInput = group.PortScanner_PortsInput; + // DNS Lookup + DNSLookup_OverrideDNSServer = group.DNSLookup_OverrideDNSServer; + DNSLookup_DNSServer = group.DNSLookup_DNSServer; + DNSLookup_DNSServerInput = group.DNSLookup_DNSServerInput; + // Remote Desktop RemoteDesktop_UseCredentials = group.RemoteDesktop_UseCredentials; RemoteDesktop_Username = group.RemoteDesktop_Username; @@ -193,6 +198,19 @@ public GroupInfo(GroupInfo group) : this(group.Name) /// public string PortScanner_PortsInput { get; set; } + public bool DNSLookup_OverrideDNSServer { get; set; } + + /// + /// DNS servers for the profiles of the group, see . + /// + public string DNSLookup_DNSServer { get; set; } + + /// + /// DNS servers as entered, if they contain DNS servers by name, see + /// . + /// + public string DNSLookup_DNSServerInput { get; set; } + public bool RemoteDesktop_UseCredentials { get; set; } public string RemoteDesktop_Username { get; set; } public string RemoteDesktop_Domain { get; set; } diff --git a/Source/NETworkManager.Profiles/GroupViewManager.cs b/Source/NETworkManager.Profiles/GroupViewManager.cs index 8b255b41c5..7758b9c6f6 100644 --- a/Source/NETworkManager.Profiles/GroupViewManager.cs +++ b/Source/NETworkManager.Profiles/GroupViewManager.cs @@ -14,6 +14,7 @@ public static class GroupViewManager // Applications new GroupViewInfo(GroupViewName.PortScanner, ApplicationManager.GetIcon(ApplicationName.PortScanner)), + new GroupViewInfo(GroupViewName.DNSLookup, ApplicationManager.GetIcon(ApplicationName.DNSLookup)), new GroupViewInfo(GroupViewName.RemoteDesktop, ApplicationManager.GetIcon(ApplicationName.RemoteDesktop)), new GroupViewInfo(GroupViewName.PowerShell, ApplicationManager.GetIcon(ApplicationName.PowerShell)), new GroupViewInfo(GroupViewName.PuTTY, ApplicationManager.GetIcon(ApplicationName.PuTTY)), diff --git a/Source/NETworkManager.Profiles/GroupViewName.cs b/Source/NETworkManager.Profiles/GroupViewName.cs index b6929f572b..153ef3d5cf 100644 --- a/Source/NETworkManager.Profiles/GroupViewName.cs +++ b/Source/NETworkManager.Profiles/GroupViewName.cs @@ -4,6 +4,7 @@ public enum GroupViewName { General, PortScanner, + DNSLookup, RemoteDesktop, PowerShell, PuTTY, diff --git a/Source/NETworkManager.Profiles/ProfileInfo.cs b/Source/NETworkManager.Profiles/ProfileInfo.cs index 18216d8cda..2a7a123263 100644 --- a/Source/NETworkManager.Profiles/ProfileInfo.cs +++ b/Source/NETworkManager.Profiles/ProfileInfo.cs @@ -1,4 +1,5 @@ //using System.Collections.Generic; +using DnsClient; using NETworkManager.Controls; using NETworkManager.Models; using NETworkManager.Models.Network; @@ -83,6 +84,8 @@ public ProfileInfo(ProfileInfo profile) DNSLookup_OverrideDNSServer = profile.DNSLookup_OverrideDNSServer; DNSLookup_DNSServer = profile.DNSLookup_DNSServer; DNSLookup_DNSServerInput = profile.DNSLookup_DNSServerInput; + DNSLookup_OverrideQueryType = profile.DNSLookup_OverrideQueryType; + DNSLookup_QueryType = profile.DNSLookup_QueryType; // Remote Desktop RemoteDesktop_Enabled = profile.RemoteDesktop_Enabled; @@ -367,6 +370,13 @@ public ProfileInfo(ProfileInfo profile) /// public string DNSLookup_DNSServerInput { get; set; } + /// + /// Use instead of the automatic type (PTR for IP addresses, otherwise A). + /// + public bool DNSLookup_OverrideQueryType { get; set; } + + public QueryType DNSLookup_QueryType { get; set; } = QueryType.A; + public bool RemoteDesktop_Enabled { get; set; } public bool RemoteDesktop_InheritHost { get; set; } = true; public string RemoteDesktop_Host { get; set; } diff --git a/Source/NETworkManager/ProfileDialogManager.cs b/Source/NETworkManager/ProfileDialogManager.cs index 602fef75fb..6935d7c513 100644 --- a/Source/NETworkManager/ProfileDialogManager.cs +++ b/Source/NETworkManager/ProfileDialogManager.cs @@ -89,6 +89,8 @@ private static ProfileInfo ParseProfileInfo(ProfileViewModel instance) SettingsManager.Current.DNSLookup_DNSServers), DNSLookup_DNSServerInput = DNSServer.EncodeReferences(instance.DNSLookup_DNSServer, SettingsManager.Current.DNSLookup_DNSServers), + DNSLookup_OverrideQueryType = instance.DNSLookup_OverrideQueryType, + DNSLookup_QueryType = instance.DNSLookup_QueryType, // Remote Desktop RemoteDesktop_Enabled = instance.RemoteDesktop_Enabled, @@ -331,6 +333,13 @@ private static GroupInfo ParseGroupInfo(GroupViewModel instance) PortScanner_PortsInput = PortProfile.EncodeReferences(instance.PortScanner_Ports, SettingsManager.Current.PortScanner_PortProfiles), + // DNS Lookup - like the profile (see ParseProfileInfo) + DNSLookup_OverrideDNSServer = instance.DNSLookup_OverrideDNSServer, + DNSLookup_DNSServer = DNSServer.ReplaceProfilesByServers(instance.DNSLookup_DNSServer?.Trim(), + SettingsManager.Current.DNSLookup_DNSServers), + DNSLookup_DNSServerInput = DNSServer.EncodeReferences(instance.DNSLookup_DNSServer, + SettingsManager.Current.DNSLookup_DNSServers), + // Remote Desktop RemoteDesktop_UseCredentials = instance.RemoteDesktop_UseCredentials, RemoteDesktop_Username = diff --git a/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs b/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs index 980b0538f0..5ca5fe8f9a 100644 --- a/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs @@ -3,9 +3,7 @@ using NETworkManager.Controls; using NETworkManager.Localization.Resources; using NETworkManager.Models; -using NETworkManager.Models.Network; using NETworkManager.Profiles; -using NETworkManager.Settings; using NETworkManager.Utilities; using NETworkManager.Views; using System; @@ -178,28 +176,12 @@ public void AddTab(string host = null, string dnsServer = null, QueryType? query /// Starts it on the first load. If false, the input is only filled in. public void AddTab(ProfileInfo profile, bool start = true) { - // DNS server of DNS Lookup (last selected) - if (!profile.DNSLookup_OverrideDNSServer || string.IsNullOrWhiteSpace(profile.DNSLookup_DNSServer)) - { - AddTab(profile.DNSLookup_Host, start: start); - return; - } - - // DNS servers by name with their current name (also after a rename) - if (!string.IsNullOrEmpty(profile.DNSLookup_DNSServerInput) && - DNSServer.TryDecodeReferences(profile.DNSLookup_DNSServerInput, - SettingsManager.Current.DNSLookup_DNSServers, out var dnsServer)) - { - AddTab(profile.DNSLookup_Host, dnsServer, start: start); - return; - } + // DNS servers of the profile or its group (null: the selected DNS server). A DNS server that can't be found: + // the saved servers, with a note. + var dnsServer = NETworkManager.Profiles.Application.DNSLookup.GetDNSServer(profile, out var dnsServerNotFound); - // No DNS server by name, or one can't be found (deleted, or a profile file shared from another computer - DNS - // servers are saved in the settings): the servers saved with the profile, with a note if one wasn't found - AddTab(profile.DNSLookup_Host, profile.DNSLookup_DNSServer, start: start, - message: string.IsNullOrEmpty(profile.DNSLookup_DNSServerInput) - ? null - : Strings.DNSServerOfProfileNotFoundMessage); + AddTab(profile.DNSLookup_Host, dnsServer, NETworkManager.Profiles.Application.DNSLookup.GetQueryType(profile), + start, dnsServerNotFound ? Strings.DNSServerOfProfileNotFoundMessage : null); } #endregion diff --git a/Source/NETworkManager/ViewModels/GroupViewModel.cs b/Source/NETworkManager/ViewModels/GroupViewModel.cs index be9f804a6b..85c2694b42 100644 --- a/Source/NETworkManager/ViewModels/GroupViewModel.cs +++ b/Source/NETworkManager/ViewModels/GroupViewModel.cs @@ -150,6 +150,48 @@ public string PortScanner_Ports #endregion + #region DNS Lookup + + public bool DNSLookup_OverrideDNSServer + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + + public string DNSLookup_DNSServer + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } + + private readonly InputSuggestions _dnsLookupDNSServerSuggestions = InputSuggestions.ForDNSLookupDNSServers(); + + /// + /// Gets the DNS servers and the DNS server history of DNS Lookup for the DNS server input (grouped). + /// + public ICollectionView DNSLookup_DNSServerSuggestions => _dnsLookupDNSServerSuggestions.View; + + /// + /// Gets the names of the DNS servers for the Tab completion of the DNS server input of DNS Lookup. + /// + public IEnumerable DNSLookup_DNSServerNames => _dnsLookupDNSServerSuggestions.ProfileNames; + + #endregion + #region Remote Desktop /// @@ -1642,6 +1684,15 @@ public GroupViewModel(Action saveCommand, Action ? portScannerPortsInput : groupInfo.PortScanner_Ports; + // DNS Lookup + DNSLookup_OverrideDNSServer = groupInfo.DNSLookup_OverrideDNSServer; + // DNS servers with their current name (also after a rename). A DNS server that can't be found (deleted, or a + // profile file from another computer): the servers saved with the group. + DNSLookup_DNSServer = DNSServer.TryDecodeReferences(groupInfo.DNSLookup_DNSServerInput, + SettingsManager.Current.DNSLookup_DNSServers, out var dnsLookupDNSServerInput) + ? dnsLookupDNSServerInput + : groupInfo.DNSLookup_DNSServer; + // Remote Desktop RemoteDesktop_UseCredentials = groupInfo.RemoteDesktop_UseCredentials; RemoteDesktop_Username = groupInfo.RemoteDesktop_Username; diff --git a/Source/NETworkManager/ViewModels/ProfileViewModel.cs b/Source/NETworkManager/ViewModels/ProfileViewModel.cs index 0de47cc35e..050ecea128 100644 --- a/Source/NETworkManager/ViewModels/ProfileViewModel.cs +++ b/Source/NETworkManager/ViewModels/ProfileViewModel.cs @@ -1,3 +1,4 @@ +using DnsClient; using NETworkManager.Controls; using NETworkManager.Localization.Resources; using NETworkManager.Models; @@ -125,6 +126,11 @@ public ProfileViewModel(Action saveCommand, Action x.ToString())]; + DNSLookup_QueryType = DNSLookup_QueryTypes.Contains(profileInfo.DNSLookup_QueryType) + ? profileInfo.DNSLookup_QueryType + : QueryType.A; // Remote Desktop RemoteDesktop_Enabled = editMode == ProfileEditMode.Add @@ -437,6 +443,8 @@ public string Host field = value; OnPropertyChanged(); + + ValidateDNSLookupQueryType(); } } @@ -902,6 +910,8 @@ public bool DNSLookup_InheritHost field = value; OnPropertyChanged(); + + ValidateDNSLookupQueryType(); } } @@ -915,6 +925,8 @@ public string DNSLookup_Host field = value; OnPropertyChanged(); + + ValidateDNSLookupQueryType(); } } @@ -956,6 +968,64 @@ public string DNSLookup_DNSServer /// public IEnumerable DNSLookup_DNSServerNames => _dnsLookupDNSServerSuggestions.ProfileNames; + public bool DNSLookup_OverrideQueryType + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + + ValidateDNSLookupQueryType(); + } + } + + public List DNSLookup_QueryTypes { get; } + + public QueryType DNSLookup_QueryType + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + + ValidateDNSLookupQueryType(); + } + } + + /// + /// Gets a value indicating whether the query type fits the host: PTR can only be looked up for IP addresses. + /// + public bool DNSLookup_QueryTypeIsValid + { + get; + private set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } = true; + + /// + /// Checks if the query type of the profile fits the host (own or inherited), see + /// . + /// + private void ValidateDNSLookupQueryType() + { + DNSLookup_QueryTypeIsValid = !DNSLookup_OverrideQueryType || DNSLookup_QueryType != QueryType.PTR || + IPAddressHelper.AreIPAddresses(DNSLookup_InheritHost ? Host : DNSLookup_Host); + } + #endregion #region Remote Desktop diff --git a/Source/NETworkManager/Views/GroupChildWindow.xaml b/Source/NETworkManager/Views/GroupChildWindow.xaml index 9517ceb1cb..f8cddccb03 100644 --- a/Source/NETworkManager/Views/GroupChildWindow.xaml +++ b/Source/NETworkManager/Views/GroupChildWindow.xaml @@ -166,6 +166,23 @@ + + + + + + + + + + + @@ -702,6 +719,61 @@ + + + + + + - - + + + - + + + + + +