diff --git a/Source/GlobalAssemblyInfo.cs b/Source/GlobalAssemblyInfo.cs
index 3e9b4307d7..23d3656a91 100644
--- a/Source/GlobalAssemblyInfo.cs
+++ b/Source/GlobalAssemblyInfo.cs
@@ -6,5 +6,5 @@
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
-[assembly: AssemblyVersion("2026.7.12.0")]
-[assembly: AssemblyFileVersion("2026.7.12.0")]
+[assembly: AssemblyVersion("2026.8.5.0")]
+[assembly: AssemblyFileVersion("2026.8.5.0")]
diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index d0d46edfd2..b1362fdc88 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -5283,21 +5283,6 @@ public static string HelpMessage_Tags {
}
}
- ///
- /// Looks up a localized string similar to This setting specifies the minimum number of threads that will be created from the application's ThreadPool on demand. This can improve the performance for example of the IP scanner or port scanner.
- ///
- ///The value is added to the default min. threads (number of CPU threads). The value 0 leaves the default settings. If the value is higher than the default max. threads of the ThreadPool, this value is used.
- ///
- ///If the value is too high, performance problems may occur.
- ///
- ///Changes to this value will take effect a [rest of string was truncated]";.
- ///
- public static string HelpMessage_ThreadPoolAdditionalMinThreads {
- get {
- return ResourceManager.GetString("HelpMessage_ThreadPoolAdditionalMinThreads", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to Use custom themes to personalize the appearance of the application. You can edit or add theme in the "Program Folder > Themes" directory. For more details, refer to the documentation..
///
@@ -6891,16 +6876,6 @@ public static string MaxPortThreads {
return ResourceManager.GetString("MaxPortThreads", resourceCulture);
}
}
-
- ///
- /// Looks up a localized string similar to These settings only change the maximum number of concurrently executed threads per host/port scan. Go to Settings > General > General to adjust the (min) threads of the application..
- ///
- public static string MaxThreadsOnlyGoToSettingsGeneralGeneral {
- get {
- return ResourceManager.GetString("MaxThreadsOnlyGoToSettingsGeneralGeneral", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to Measured time.
///
@@ -11944,15 +11919,6 @@ public static string ThisWillResetAllSettings {
}
}
- ///
- /// Looks up a localized string similar to ThreadPool additional min. threads.
- ///
- public static string ThreadPoolAdditionalMinThreads {
- get {
- return ResourceManager.GetString("ThreadPoolAdditionalMinThreads", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to Threads.
///
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index 1157005191..0c2584cd75 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -3353,21 +3353,6 @@ If the option is disabled again, the values are no longer modified. However, the
Ping status
-
- ThreadPool additional min. threads
-
-
- This setting specifies the minimum number of threads that will be created from the application's ThreadPool on demand. This can improve the performance for example of the IP scanner or port scanner.
-
-The value is added to the default min. threads (number of CPU threads). The value 0 leaves the default settings. If the value is higher than the default max. threads of the ThreadPool, this value is used.
-
-If the value is too high, performance problems may occur.
-
-Changes to this value will take effect after the application is restarted. Whether the value was set successfully can be seen in the log file under %LocalAppData%\NETworkManager\NETworkManager.log
-
-
- These settings only change the maximum number of concurrently executed threads per host/port scan. Go to Settings > General > General to adjust the (min) threads of the application.
-
Port status
diff --git a/Source/NETworkManager.Models/Network/HostRangeHelper.cs b/Source/NETworkManager.Models/Network/HostRangeHelper.cs
index 43ccdc505b..f7b21cadef 100644
--- a/Source/NETworkManager.Models/Network/HostRangeHelper.cs
+++ b/Source/NETworkManager.Models/Network/HostRangeHelper.cs
@@ -29,19 +29,14 @@ public static IEnumerable CreateListFromInput(string hosts)
.ToArray();
}
- public static Task<(List<(IPAddress ipAddress, string hostname)> hosts, List hostnamesNotResolved)>
+ public static async Task<(List<(IPAddress ipAddress, string hostname)> hosts, List hostnamesNotResolved)>
ResolveAsync(IEnumerable hosts, bool dnsResolveHostnamePreferIPv4, CancellationToken cancellationToken)
- {
- return Task.Run(() => Resolve(hosts, dnsResolveHostnamePreferIPv4, cancellationToken), cancellationToken);
- }
-
- private static (List<(IPAddress ipAddress, string hostname)> hosts, List hostnamesNotResolved) Resolve(
- IEnumerable hosts, bool dnsResolveHostnamePreferIPv4, CancellationToken cancellationToken)
{
var hostsBag = new ConcurrentBag<(IPAddress ipAddress, string hostname)>();
var hostnamesNotResovledBag = new ConcurrentBag();
- Parallel.ForEach(hosts, new ParallelOptions { CancellationToken = cancellationToken }, host =>
+ await Parallel.ForEachAsync(hosts, new ParallelOptions { CancellationToken = cancellationToken },
+ async (host, ct) =>
{
switch (host)
{
@@ -62,7 +57,7 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List
Parallel.For(IPv4Address.ToInt32(network.Network), IPv4Address.ToInt32(network.Broadcast) + 1,
(i, state) =>
{
- if (cancellationToken.IsCancellationRequested)
+ if (ct.IsCancellationRequested)
state.Break();
hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
@@ -77,7 +72,7 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List
Parallel.For(IPv4Address.ToInt32(IPAddress.Parse(range[0])),
IPv4Address.ToInt32(IPAddress.Parse(range[1])) + 1, (i, state) =>
{
- if (cancellationToken.IsCancellationRequested)
+ if (ct.IsCancellationRequested)
state.Break();
hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
@@ -107,7 +102,7 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List
Parallel.For(int.Parse(rangeNumbers[0]), int.Parse(rangeNumbers[1]) + 1,
(i, state) =>
{
- if (cancellationToken.IsCancellationRequested)
+ if (ct.IsCancellationRequested)
state.Break();
innerList.Add(i);
@@ -124,18 +119,18 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List
}
// Build the new ipv4
- Parallel.ForEach(list[0], new ParallelOptions { CancellationToken = cancellationToken },
+ Parallel.ForEach(list[0], new ParallelOptions { CancellationToken = ct },
i =>
{
- Parallel.ForEach(list[1], new ParallelOptions { CancellationToken = cancellationToken },
+ Parallel.ForEach(list[1], new ParallelOptions { CancellationToken = ct },
j =>
{
Parallel.ForEach(list[2],
- new ParallelOptions { CancellationToken = cancellationToken },
+ new ParallelOptions { CancellationToken = ct },
k =>
{
Parallel.ForEach(list[3],
- new ParallelOptions { CancellationToken = cancellationToken },
+ new ParallelOptions { CancellationToken = ct },
h =>
{
hostsBag.Add((IPAddress.Parse($"{i}.{j}.{k}.{h}"), string.Empty));
@@ -148,17 +143,13 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List
// example.com
case var _ when RegexHelper.HostnameOrDomainRegex().IsMatch(host):
- using (var dnsResolverTask =
- DNSClientHelper.ResolveAorAaaaAsync(host, dnsResolveHostnamePreferIPv4))
- {
- // Wait for task inside a Parallel.Foreach
- dnsResolverTask.Wait(cancellationToken);
+ var dnsResult = await DNSClientHelper.ResolveAorAaaaAsync(host, dnsResolveHostnamePreferIPv4)
+ .WaitAsync(ct).ConfigureAwait(false);
- if (!dnsResolverTask.Result.HasError)
- hostsBag.Add((IPAddress.Parse($"{dnsResolverTask.Result.Value}"), host));
- else
- hostnamesNotResovledBag.Add(host);
- }
+ if (!dnsResult.HasError)
+ hostsBag.Add((IPAddress.Parse($"{dnsResult.Value}"), host));
+ else
+ hostnamesNotResovledBag.Add(host);
break;
@@ -168,42 +159,39 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List
var hostAndSubnet = host.Split('/');
// Only support IPv4
- using (var dnsResolverTask = DNSClientHelper.ResolveAorAaaaAsync(hostAndSubnet[0], true))
- {
- // Wait for task inside a Parallel.Foreach
- dnsResolverTask.Wait(cancellationToken);
+ var dnsResultWithSubnet = await DNSClientHelper.ResolveAorAaaaAsync(hostAndSubnet[0], true)
+ .WaitAsync(ct).ConfigureAwait(false);
- if (!dnsResolverTask.Result.HasError)
+ if (!dnsResultWithSubnet.HasError)
+ {
+ // Only support IPv4 for ranges for now
+ if (dnsResultWithSubnet.Value.AddressFamily == AddressFamily.InterNetwork)
{
- // Only support IPv4 for ranges for now
- if (dnsResolverTask.Result.Value.AddressFamily == AddressFamily.InterNetwork)
- {
- network = IPNetwork2.Parse(
- $"{dnsResolverTask.Result.Value}/{hostAndSubnet[1]}");
-
- Parallel.For(IPv4Address.ToInt32(network.Network),
- IPv4Address.ToInt32(network.Broadcast) + 1, (i, state) =>
- {
- if (cancellationToken.IsCancellationRequested)
- state.Break();
-
- hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
- });
- }
- else
- {
- hostnamesNotResovledBag.Add(hostAndSubnet[0]);
- }
+ network = IPNetwork2.Parse(
+ $"{dnsResultWithSubnet.Value}/{hostAndSubnet[1]}");
+
+ Parallel.For(IPv4Address.ToInt32(network.Network),
+ IPv4Address.ToInt32(network.Broadcast) + 1, (i, state) =>
+ {
+ if (ct.IsCancellationRequested)
+ state.Break();
+
+ hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
+ });
}
else
{
hostnamesNotResovledBag.Add(hostAndSubnet[0]);
}
}
+ else
+ {
+ hostnamesNotResovledBag.Add(hostAndSubnet[0]);
+ }
break;
}
- });
+ }).ConfigureAwait(false);
// Sort list and return
IPAddressComparer comparer = new();
diff --git a/Source/NETworkManager.Models/Network/IPScanner.cs b/Source/NETworkManager.Models/Network/IPScanner.cs
index 7fb4e8b698..039fc66482 100644
--- a/Source/NETworkManager.Models/Network/IPScanner.cs
+++ b/Source/NETworkManager.Models/Network/IPScanner.cs
@@ -79,7 +79,7 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts,
CancellationToken cancellationToken)
{
// Start the scan in a separate task
- Task.Run(() =>
+ Task.Run(async () =>
{
_progressValue = 0;
@@ -101,40 +101,32 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts,
};
// Start scan
- Parallel.ForEach(hosts, hostParallelOptions, host =>
+ await Parallel.ForEachAsync(hosts, hostParallelOptions, async (host, ct) =>
{
- // Start ping async
- var pingTask = PingAsync(host.ipAddress, cancellationToken);
+ // Start ping, port scan and netbios lookup concurrently - none of these block a thread anymore
+ var pingTask = PingAsync(host.ipAddress, ct);
- // Start port scan async (if enabled)
var portScanTask = options.PortScanEnabled
- ? PortScanAsync(host.ipAddress, portScanParallelOptions, cancellationToken)
- : Task.FromResult(Enumerable.Empty());
+ ? PortScanAsync(host.ipAddress, portScanParallelOptions, ct)
+ : Task.FromResult(new List());
- // Start netbios lookup async (if enabled)
var netbiosTask = options.NetBIOSEnabled
- ? NetBIOSResolver.ResolveAsync(host.ipAddress, options.NetBIOSTimeout, cancellationToken)
+ ? NetBIOSResolver.ResolveAsync(host.ipAddress, options.NetBIOSTimeout, ct)
: Task.FromResult(new NetBIOSInfo(host.ipAddress));
- // Get ping result
- pingTask.Wait(cancellationToken);
- var pingInfo = pingTask.Result;
-
- // Get port scan result
- portScanTask.Wait(cancellationToken);
- var portScanResults = portScanTask.Result.ToList();
+ await Task.WhenAll(pingTask, portScanTask, netbiosTask).ConfigureAwait(false);
- // Get netbios result
- netbiosTask.Wait(cancellationToken);
+ var pingInfo = pingTask.Result;
+ var portScanResults = portScanTask.Result;
var netBIOSInfo = netbiosTask.Result;
// Cancel if the user has canceled
- cancellationToken.ThrowIfCancellationRequested();
+ ct.ThrowIfCancellationRequested();
// Check if host is up
var isAnyPortOpen = portScanResults.Any(x => x.State == PortState.Open);
var isReachable = pingInfo.Status == IPStatus.Success || // ICMP response
- isAnyPortOpen || // Any port is open
+ isAnyPortOpen || // Any port is open
netBIOSInfo.IsReachable; // NetBIOS response
// DNS & ARP
@@ -145,14 +137,11 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts,
if (options.ResolveHostname)
{
- // Don't use await in Parallel.ForEach, this will break
- var dnsResolverTask = DNSClient.GetInstance().ResolvePtrAsync(host.ipAddress);
-
- // Wait for task inside a Parallel.Foreach
- dnsResolverTask.Wait(cancellationToken);
+ var dnsResult = await DNSClient.GetInstance().ResolvePtrAsync(host.ipAddress)
+ .WaitAsync(ct).ConfigureAwait(false);
- if (!dnsResolverTask.Result.HasError)
- dnsHostname = dnsResolverTask.Result.Value;
+ if (!dnsResult.HasError)
+ dnsHostname = dnsResult.Value;
}
// ARP
@@ -212,7 +201,7 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts,
}
IncreaseProgress();
- });
+ }).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -225,101 +214,79 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts,
}, cancellationToken);
}
- private Task PingAsync(IPAddress ipAddress, CancellationToken cancellationToken)
+ private async Task PingAsync(IPAddress ipAddress, CancellationToken cancellationToken)
{
- return Task.Run(() =>
+ using var ping = new System.Net.NetworkInformation.Ping();
+
+ for (var i = 0; i < options.ICMPAttempts; i++)
{
- using var ping = new System.Net.NetworkInformation.Ping();
+ // Get timestamp
+ var timestamp = DateTime.Now;
- for (var i = 0; i < options.ICMPAttempts; i++)
+ try
{
- // Get timestamp
- var timestamp = DateTime.Now;
+ // Note: the CancellationToken-accepting overload requires a TimeSpan timeout,
+ // unlike the legacy int-based overloads used elsewhere in .NET's Ping API.
+ var pingReply = await ping.SendPingAsync(ipAddress, TimeSpan.FromMilliseconds(options.ICMPTimeout),
+ options.ICMPBuffer, cancellationToken: cancellationToken).ConfigureAwait(false);
- try
+ // Success
+ if (pingReply is { Status: IPStatus.Success })
{
- var pingReply = ping.Send(ipAddress, options.ICMPTimeout, options.ICMPBuffer);
-
- // Success
- if (pingReply is { Status: IPStatus.Success })
+ switch (ipAddress.AddressFamily)
{
- switch (ipAddress.AddressFamily)
- {
- case AddressFamily.InterNetwork:
- return new PingInfo(
- timestamp,
- pingReply.Address,
- pingReply.Buffer.Length,
- pingReply.RoundtripTime,
- pingReply.Options!.Ttl,
- pingReply.Status);
- case AddressFamily.InterNetworkV6:
- return new PingInfo(
+ case AddressFamily.InterNetwork:
+ return new PingInfo(
timestamp,
pingReply.Address,
pingReply.Buffer.Length,
pingReply.RoundtripTime,
+ pingReply.Options!.Ttl,
pingReply.Status);
- }
+ case AddressFamily.InterNetworkV6:
+ return new PingInfo(
+ timestamp,
+ pingReply.Address,
+ pingReply.Buffer.Length,
+ pingReply.RoundtripTime,
+ pingReply.Status);
}
-
- // Failed
- if (pingReply != null)
- return new PingInfo(timestamp, ipAddress, pingReply.Status);
- }
- catch (PingException)
- {
- // Ping failed with unknown status
- return new PingInfo(timestamp, ipAddress, IPStatus.Unknown);
}
- // Don't scan again, if the user has canceled (when more than 1 attempt)
- if (cancellationToken.IsCancellationRequested)
- break;
+ // Failed
+ if (pingReply != null)
+ return new PingInfo(timestamp, ipAddress, pingReply.Status);
+ }
+ catch (PingException)
+ {
+ // Ping failed with unknown status
+ return new PingInfo(timestamp, ipAddress, IPStatus.Unknown);
}
- // Fall back to unknown status
- return new PingInfo(DateTime.Now, ipAddress, IPStatus.Unknown);
- }, cancellationToken);
+ // Don't scan again, if the user has canceled (when more than 1 attempt)
+ if (cancellationToken.IsCancellationRequested)
+ break;
+ }
+
+ // Fall back to unknown status
+ return new PingInfo(DateTime.Now, ipAddress, IPStatus.Unknown);
}
- private Task> PortScanAsync(IPAddress ipAddress, ParallelOptions parallelOptions,
+ private async Task> PortScanAsync(IPAddress ipAddress, ParallelOptions parallelOptions,
CancellationToken cancellationToken)
{
ConcurrentBag results = [];
- Parallel.ForEach(options.PortScanPorts, parallelOptions, port =>
+ await Parallel.ForEachAsync(options.PortScanPorts, parallelOptions, async (port, ct) =>
{
- // Test if port is open
- using var tcpClient = new TcpClient(ipAddress.AddressFamily);
+ var portState = await PortProbe.ProbeAsync(ipAddress, port, options.PortScanTimeout, ct)
+ .ConfigureAwait(false);
- var portState = PortState.None;
-
- try
- {
- // ReSharper disable once MethodSupportsCancellation - Wait for timeout
- var task = tcpClient.ConnectAsync(ipAddress, port);
-
- if (task.Wait(options.PortScanTimeout, cancellationToken))
- portState = tcpClient.Connected ? PortState.Open : PortState.Closed;
- else
- portState = PortState.TimedOut;
- }
- catch
- {
- portState = PortState.Closed;
- }
- finally
- {
- tcpClient.Close();
-
- if (portState == PortState.Open || options.ShowAllResults)
- results.Add(
- new PortInfo(port, PortLookup.LookupByPortAndProtocol(port), portState));
- }
- });
+ if (portState == PortState.Open || options.ShowAllResults)
+ results.Add(new PortInfo(port, PortLookup.LookupByPortAndProtocol(port), portState));
+ }).ConfigureAwait(false);
- return Task.FromResult(results.AsEnumerable());
+ return results.ToList();
}
private void IncreaseProgress()
diff --git a/Source/NETworkManager.Models/Network/NetBIOSResolver.cs b/Source/NETworkManager.Models/Network/NetBIOSResolver.cs
index 342de111fe..ca44faf100 100644
--- a/Source/NETworkManager.Models/Network/NetBIOSResolver.cs
+++ b/Source/NETworkManager.Models/Network/NetBIOSResolver.cs
@@ -47,21 +47,20 @@ public static async Task ResolveAsync(IPAddress ipAddress, int time
CancellationToken cancellationToken)
{
var udpClient = new UdpClient();
- udpClient.Client.ReceiveTimeout = timeout;
var remoteEndPoint = new IPEndPoint(ipAddress, NetBIOSUdpPort);
+ // Linked so the receive is genuinely canceled (not just abandoned) on either timeout or
+ // caller cancellation - avoids leaving a pending receive that later faults, unobserved,
+ // once the socket is closed in the finally block below.
+ using var timeoutCts = new CancellationTokenSource(timeout);
+ using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
+
try
{
await udpClient.SendAsync(RequestData, RequestData.Length, remoteEndPoint);
- // ReSharper disable once MethodSupportsCancellation - cancellation is handled below by Task.WhenAny
- var receiveTask = udpClient.ReceiveAsync();
-
- if (!receiveTask.Wait(timeout, cancellationToken))
- return new NetBIOSInfo(ipAddress);
-
- var response = receiveTask.Result;
+ var response = await udpClient.ReceiveAsync(linkedCts.Token).ConfigureAwait(false);
if (response.Buffer.Length < ResponseBaseLen || response.Buffer[ResponseTypePos] != ResponseTypeNbstat)
return new NetBIOSInfo(ipAddress); // response was too short
diff --git a/Source/NETworkManager.Models/Network/PortProbe.cs b/Source/NETworkManager.Models/Network/PortProbe.cs
new file mode 100644
index 0000000000..6e2096abba
--- /dev/null
+++ b/Source/NETworkManager.Models/Network/PortProbe.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace NETworkManager.Models.Network;
+
+///
+/// Provides a shared, fully asynchronous TCP connect probe used by and
+/// .
+///
+internal static class PortProbe
+{
+ ///
+ /// Attempts a TCP connect to the given and and
+ /// classifies the result. Never throws for expected connect failures.
+ ///
+ /// IP address to connect to.
+ /// Port to connect to.
+ /// Timeout in milliseconds after which the port is considered timed out.
+ /// Token to monitor for cancellation requests.
+ /// The of the probed port.
+ public static async Task ProbeAsync(IPAddress ipAddress, int port, int timeoutMs,
+ CancellationToken cancellationToken)
+ {
+ using var tcpClient = new TcpClient(ipAddress.AddressFamily);
+ using var timeoutCts = new CancellationTokenSource(timeoutMs);
+ using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
+
+ try
+ {
+ await tcpClient.ConnectAsync(ipAddress, port, linkedCts.Token).ConfigureAwait(false);
+
+ return tcpClient.Connected ? PortState.Open : PortState.Closed;
+ }
+ catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested &&
+ !cancellationToken.IsCancellationRequested)
+ {
+ // Only our own timeout fired, not the caller's cancellation -> timed out
+ return PortState.TimedOut;
+ }
+ catch (Exception) when (!cancellationToken.IsCancellationRequested)
+ {
+ // Connection refused, host unreachable, etc.
+ return PortState.Closed;
+ }
+ finally
+ {
+ tcpClient.Close();
+ }
+ }
+}
diff --git a/Source/NETworkManager.Models/Network/PortProfile.cs b/Source/NETworkManager.Models/Network/PortProfile.cs
index 606efa67d3..f124078cef 100644
--- a/Source/NETworkManager.Models/Network/PortProfile.cs
+++ b/Source/NETworkManager.Models/Network/PortProfile.cs
@@ -18,7 +18,8 @@ public static List GetDefaultList()
new("Database", "1433-1434; 1521; 1830; 3306; 5432"),
new("SMB", "139; 445"),
new("LDAP", "389; 636"),
- new("HTTP proxy", "3128")
+ new("HTTP proxy", "3128"),
+ new("Well-known ports", "1-1024")
};
}
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/Network/PortScanner.cs b/Source/NETworkManager.Models/Network/PortScanner.cs
index 372cd2d861..fff1c51681 100644
--- a/Source/NETworkManager.Models/Network/PortScanner.cs
+++ b/Source/NETworkManager.Models/Network/PortScanner.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net;
-using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using NETworkManager.Models.Lookup;
@@ -67,7 +66,7 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts,
{
_progressValue = 0;
- Task.Run(() =>
+ Task.Run(async () =>
{
try
{
@@ -83,58 +82,34 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts,
MaxDegreeOfParallelism = _options.MaxPortThreads
};
- Parallel.ForEach(hosts, hostParallelOptions, host =>
+ await Parallel.ForEachAsync(hosts, hostParallelOptions, async (host, hostCt) =>
{
// Resolve Hostname (PTR)
var hostname = string.Empty;
if (_options.ResolveHostname)
{
- // Don't use await in Parallel.ForEach, this will break
- var dnsResolverTask = DNSClient.GetInstance().ResolvePtrAsync(host.ipAddress);
+ var dnsResult = await DNSClient.GetInstance().ResolvePtrAsync(host.ipAddress)
+ .WaitAsync(hostCt).ConfigureAwait(false);
- // Wait for task inside a Parallel.Foreach
- dnsResolverTask.Wait(cancellationToken);
-
- if (!dnsResolverTask.Result.HasError)
- hostname = dnsResolverTask.Result.Value;
+ if (!dnsResult.HasError)
+ hostname = dnsResult.Value;
}
// Check each port
- Parallel.ForEach(ports, portParallelOptions, port =>
+ await Parallel.ForEachAsync(ports, portParallelOptions, async (port, portCt) =>
{
- // Test if port is open
- using (var tcpClient = new TcpClient(host.ipAddress.AddressFamily))
- {
- var portState = PortState.None;
-
- try
- {
- var task = tcpClient.ConnectAsync(host.ipAddress, port);
-
- if (task.Wait(_options.Timeout))
- portState = tcpClient.Connected ? PortState.Open : PortState.Closed;
- else
- portState = PortState.TimedOut;
- }
- catch
- {
- portState = PortState.Closed;
- }
- finally
- {
- tcpClient.Close();
-
- if (_options.ShowAllResults || portState == PortState.Open)
- OnPortScanned(new PortScannerPortScannedArgs(
- new PortScannerPortInfo(host.ipAddress, hostname, port,
- PortLookup.LookupByPortAndProtocol(port), portState)));
- }
- }
+ var portState = await PortProbe.ProbeAsync(host.ipAddress, port, _options.Timeout, portCt)
+ .ConfigureAwait(false);
+
+ if (_options.ShowAllResults || portState == PortState.Open)
+ OnPortScanned(new PortScannerPortScannedArgs(
+ new PortScannerPortInfo(host.ipAddress, hostname, port,
+ PortLookup.LookupByPortAndProtocol(port), portState)));
IncreaseProgress();
- });
- });
+ }).ConfigureAwait(false);
+ }).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
index d71cb6aab6..c7b9c5adaf 100644
--- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
+++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
@@ -61,7 +61,6 @@ public static class GlobalStaticConfiguration
// Settings: General
public static int General_BackgroundJobInterval => 5;
- public static int General_ThreadPoolAdditionalMinThreads => 512;
public static int General_HistoryListEntries => 10;
// Settings: Window
@@ -132,17 +131,17 @@ 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; 139; 389; 636; 443; 445; 3389";
+ public static string IPScanner_PortScanPorts => "22; 53; 80; 135; 139; 389; 636; 443; 445; 3389; 9100";
public static int IPScanner_PortScanTimeout => 4000;
- public static int IPScanner_MaxHostThreads => 256;
- public static int IPScanner_MaxPortThreads => 5;
+ public static int IPScanner_MaxHostThreads => 64;
+ public static int IPScanner_MaxPortThreads => 4;
public static bool IPScanner_NetBIOSEnabled => true;
public static int IPScanner_NetBIOSTimeout => 4000;
public static ExportFileType IPScanner_ExportFileType => ExportFileType.Csv;
// Application: Port Scanner
- public static int PortScanner_MaxHostThreads => 5;
- public static int PortScanner_MaxPortThreads => 256;
+ public static int PortScanner_MaxHostThreads => 4;
+ public static int PortScanner_MaxPortThreads => 64;
public static int PortScanner_Timeout => 4000;
public static ExportFileType PortScanner_ExportFileType => ExportFileType.Csv;
diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs
index eb33ec907c..ae78719a25 100644
--- a/Source/NETworkManager.Settings/SettingsInfo.cs
+++ b/Source/NETworkManager.Settings/SettingsInfo.cs
@@ -152,20 +152,6 @@ public int General_BackgroundJobInterval
}
} = GlobalStaticConfiguration.General_BackgroundJobInterval;
-
- public int General_ThreadPoolAdditionalMinThreads
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- } = GlobalStaticConfiguration.General_ThreadPoolAdditionalMinThreads;
-
public int General_HistoryListEntries
{
get;
diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs
index 93da62186f..519a0f7777 100644
--- a/Source/NETworkManager.Settings/SettingsManager.cs
+++ b/Source/NETworkManager.Settings/SettingsManager.cs
@@ -741,6 +741,47 @@ private static void UpgradeTo_2026_7_7_0()
private static void UpgradeToLatest(Version version)
{
Log.Info($"Apply upgrade to {version}...");
+
+ // IP Scanner / Port Scanner - lower the default concurrency (see changelog for why).
+ // Only applied if still at the old default, so a deliberately customized value is left alone.
+ Log.Info("Lower IP Scanner / Port Scanner default concurrency, if still unchanged from the old default...");
+
+ if (Current.IPScanner_MaxHostThreads == 256)
+ {
+ Log.Info("Update \"IPScanner_MaxHostThreads\" from 256 to 64...");
+ Current.IPScanner_MaxHostThreads = 64;
+ }
+
+ if (Current.IPScanner_MaxPortThreads == 5)
+ {
+ Log.Info("Update \"IPScanner_MaxPortThreads\" from 5 to 4...");
+ Current.IPScanner_MaxPortThreads = 4;
+ }
+
+ if (Current.PortScanner_MaxHostThreads == 5)
+ {
+ Log.Info("Update \"PortScanner_MaxHostThreads\" from 5 to 4...");
+ Current.PortScanner_MaxHostThreads = 4;
+ }
+
+ if (Current.PortScanner_MaxPortThreads == 256)
+ {
+ Log.Info("Update \"PortScanner_MaxPortThreads\" from 256 to 64...");
+ Current.PortScanner_MaxPortThreads = 64;
+ }
+
+ // Add new Port Scanner port profiles
+ foreach (var portProfile in PortProfile.GetDefaultList())
+ {
+ var portProfileFound =
+ Current.PortScanner_PortProfiles.FirstOrDefault(x => x.Name == portProfile.Name);
+
+ if (portProfileFound != null)
+ continue;
+
+ Log.Info($"Add \"{portProfile.Name}\" to \"PortScanner_PortProfiles\"...");
+ Current.PortScanner_PortProfiles.Add(portProfile);
+ }
}
#endregion
}
diff --git a/Source/NETworkManager/App.xaml.cs b/Source/NETworkManager/App.xaml.cs
index 74ac6812a3..d041c1c105 100644
--- a/Source/NETworkManager/App.xaml.cs
+++ b/Source/NETworkManager/App.xaml.cs
@@ -165,27 +165,6 @@ by BornToBeRoot
Log.Info("Background job is disabled.");
}
- // Setup ThreadPool for the application
- ThreadPool.GetMaxThreads(out var workerThreadsMax, out var completionPortThreadsMax);
- ThreadPool.GetMinThreads(out var workerThreadsMin, out var completionPortThreadsMin);
-
- var workerThreadsMinNew = workerThreadsMin + SettingsManager.Current.General_ThreadPoolAdditionalMinThreads;
- var completionPortThreadsMinNew = completionPortThreadsMin +
- SettingsManager.Current.General_ThreadPoolAdditionalMinThreads;
-
- if (workerThreadsMinNew > workerThreadsMax)
- workerThreadsMinNew = workerThreadsMax;
-
- if (completionPortThreadsMinNew > completionPortThreadsMax)
- completionPortThreadsMinNew = completionPortThreadsMax;
-
- if (ThreadPool.SetMinThreads(workerThreadsMinNew, completionPortThreadsMinNew))
- Log.Info(
- $"ThreadPool min threads set to: workerThreads: {workerThreadsMinNew}, completionPortThreads: {completionPortThreadsMinNew}");
- else
- Log.Warn(
- $"ThreadPool min threads could not be set to workerThreads: {workerThreadsMinNew}, completionPortThreads: {completionPortThreadsMinNew}");
-
// Show splash screen
if (SettingsManager.Current.SplashScreen_Enabled)
{
diff --git a/Source/NETworkManager/ViewModels/IPScannerViewModel.cs b/Source/NETworkManager/ViewModels/IPScannerViewModel.cs
index c5321a0d50..388ceb5a64 100644
--- a/Source/NETworkManager/ViewModels/IPScannerViewModel.cs
+++ b/Source/NETworkManager/ViewModels/IPScannerViewModel.cs
@@ -43,6 +43,18 @@ public class IPScannerViewModel : ViewModelBase, IProfileManagerMinimal
private bool _firstLoad = true;
private bool _closed;
+ // Background HostScanned events append here instead of hopping to the UI thread per item -
+ // a DispatcherTimer periodically flushes this into the Results collection instead, so a large
+ // scan doesn't flood the dispatcher queue with one BeginInvoke per host.
+ private readonly List _resultsBuffer = [];
+ private readonly Lock _resultsBufferLock = new();
+ private DispatcherTimer _resultsFlushTimer;
+
+ // Same reasoning as the results buffer above - ProgressChanged fires once per host
+ // (unconditionally, unlike HostScanned), so it's flushed to the bound property on the same
+ // timer instead of updating it directly from the background thread on every event.
+ private int _latestHostsScanned;
+
///
/// Gets or sets the host or IP range to scan.
///
@@ -435,6 +447,13 @@ private async Task Start()
IsRunning = true;
PreparingScan = true;
+ _resultsFlushTimer?.Stop();
+
+ lock (_resultsBufferLock)
+ {
+ _resultsBuffer.Clear();
+ }
+
Results.Clear();
DragablzTabItem.SetTabHeader(_tabId, Host);
@@ -466,6 +485,7 @@ private async Task Start()
HostsToScan = hosts.hosts.Count;
HostsScanned = 0;
+ Volatile.Write(ref _latestHostsScanned, 0);
PreparingScan = false;
@@ -493,6 +513,17 @@ await PortRangeHelper.ConvertPortRangeToIntArrayAsync(SettingsManager.Current.IP
ipScanner.ProgressChanged += ProgressChanged;
ipScanner.UserHasCanceled += UserHasCanceled;
+ _resultsFlushTimer = new DispatcherTimer(DispatcherPriority.Background)
+ {
+ Interval = TimeSpan.FromMilliseconds(150)
+ };
+ _resultsFlushTimer.Tick += (_, _) =>
+ {
+ FlushResultsBuffer();
+ FlushProgress();
+ };
+ _resultsFlushTimer.Start();
+
ipScanner.ScanAsync(hosts.hosts, _cancellationTokenSource.Token);
}
@@ -704,21 +735,53 @@ public void OnClose()
/// The instance containing the event data.
private void HostScanned(object sender, IPScannerHostScannedArgs e)
{
- Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
- new Action(delegate
- {
- Results.Add(e.Args);
- }));
+ lock (_resultsBufferLock)
+ {
+ _resultsBuffer.Add(e.Args);
+ }
}
///
- /// Handles the ProgressChanged event. Updates the progress.
+ /// Moves buffered scan results into . Always called on the UI thread -
+ /// either from the tick, or from within a Dispatcher.Invoke
+ /// call in / .
+ ///
+ private void FlushResultsBuffer()
+ {
+ List itemsToAdd;
+
+ lock (_resultsBufferLock)
+ {
+ if (_resultsBuffer.Count == 0)
+ return;
+
+ itemsToAdd = [.. _resultsBuffer];
+ _resultsBuffer.Clear();
+ }
+
+ foreach (var item in itemsToAdd)
+ Results.Add(item);
+ }
+
+ ///
+ /// Handles the ProgressChanged event. Stores the value for to
+ /// pick up on the next timer tick, instead of updating the bound property directly from a
+ /// background thread on every single host.
///
/// The source of the event.
/// The instance containing the event data.
private void ProgressChanged(object sender, ProgressChangedArgs e)
{
- HostsScanned = e.Value;
+ Volatile.Write(ref _latestHostsScanned, e.Value);
+ }
+
+ ///
+ /// Pushes the latest buffered progress value into . Always called
+ /// on the UI thread - same calling contexts as .
+ ///
+ private void FlushProgress()
+ {
+ HostsScanned = Volatile.Read(ref _latestHostsScanned);
}
///
@@ -732,6 +795,10 @@ private void ScanComplete(object sender, EventArgs e)
// to ensure all results are added first #3285
Application.Current.Dispatcher.Invoke(() =>
{
+ _resultsFlushTimer?.Stop();
+ FlushResultsBuffer();
+ FlushProgress();
+
if (Results.Count == 0)
{
StatusMessage = Strings.NoReachableHostsFound;
@@ -750,11 +817,18 @@ private void ScanComplete(object sender, EventArgs e)
/// The instance containing the event data.
private void UserHasCanceled(object sender, EventArgs e)
{
- StatusMessage = Strings.CanceledByUserMessage;
- IsStatusMessageDisplayed = true;
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ _resultsFlushTimer?.Stop();
+ FlushResultsBuffer();
+ FlushProgress();
- IsCanceling = false;
- IsRunning = false;
+ StatusMessage = Strings.CanceledByUserMessage;
+ IsStatusMessageDisplayed = true;
+
+ IsCanceling = false;
+ IsRunning = false;
+ });
}
#endregion
diff --git a/Source/NETworkManager/ViewModels/PortProfilesViewModel.cs b/Source/NETworkManager/ViewModels/PortProfilesViewModel.cs
index 1ea72bf4c1..06a21f8033 100644
--- a/Source/NETworkManager/ViewModels/PortProfilesViewModel.cs
+++ b/Source/NETworkManager/ViewModels/PortProfilesViewModel.cs
@@ -26,8 +26,17 @@ public class PortProfilesViewModel : ViewModelBase
/// The action to execute when the Cancel command is invoked.
public PortProfilesViewModel(Action okCommand, Action cancelHandler)
{
- OKCommand = new RelayCommand(_ => okCommand(this));
- CancelCommand = new RelayCommand(_ => cancelHandler(this));
+ // Clear the filter on close so it doesn't leak into other views sharing this collection's default view.
+ OKCommand = new RelayCommand(_ =>
+ {
+ PortProfiles.Filter = null;
+ okCommand(this);
+ });
+ CancelCommand = new RelayCommand(_ =>
+ {
+ PortProfiles.Filter = null;
+ cancelHandler(this);
+ });
PortProfiles = CollectionViewSource.GetDefaultView(SettingsManager.Current.PortScanner_PortProfiles);
PortProfiles.SortDescriptions.Add(
diff --git a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs
index f683de1179..44f9f26e8e 100644
--- a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs
+++ b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs
@@ -38,6 +38,18 @@ public class PortScannerViewModel : ViewModelBase
private bool _firstLoad = true;
private bool _closed;
+ // Background PortScanned events append here instead of hopping to the UI thread per item -
+ // a DispatcherTimer periodically flushes this into the Results collection instead, so a large
+ // scan doesn't flood the dispatcher queue with one BeginInvoke per port.
+ private readonly List _resultsBuffer = [];
+ private readonly Lock _resultsBufferLock = new();
+ private DispatcherTimer _resultsFlushTimer;
+
+ // Same reasoning as the results buffer above - ProgressChanged fires once per port
+ // (unconditionally, unlike PortScanned), so it's flushed to the bound property on the same
+ // timer instead of updating it directly from the background thread on every event.
+ private int _latestPortsScanned;
+
///
/// Gets or sets the host to scan.
///
@@ -386,6 +398,13 @@ private async Task Start()
IsRunning = true;
PreparingScan = true;
+ _resultsFlushTimer?.Stop();
+
+ lock (_resultsBufferLock)
+ {
+ _resultsBuffer.Clear();
+ }
+
Results.Clear();
DragablzTabItem.SetTabHeader(_tabId, Host);
@@ -420,6 +439,7 @@ private async Task Start()
PortsToScan = ports.Length * hosts.hosts.Count;
PortsScanned = 0;
+ Volatile.Write(ref _latestPortsScanned, 0);
PreparingScan = false;
@@ -440,6 +460,17 @@ private async Task Start()
portScanner.ProgressChanged += ProgressChanged;
portScanner.UserHasCanceled += UserHasCanceled;
+ _resultsFlushTimer = new DispatcherTimer(DispatcherPriority.Background)
+ {
+ Interval = TimeSpan.FromMilliseconds(150)
+ };
+ _resultsFlushTimer.Tick += (_, _) =>
+ {
+ FlushResultsBuffer();
+ FlushProgress();
+ };
+ _resultsFlushTimer.Start();
+
portScanner.ScanAsync(hosts.hosts, ports, _cancellationTokenSource.Token);
}
@@ -531,13 +562,46 @@ private void AddPortToHistory(string port)
private void PortScanned(object sender, PortScannerPortScannedArgs e)
{
- Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
- new Action(delegate { Results.Add(e.Args); }));
+ lock (_resultsBufferLock)
+ {
+ _resultsBuffer.Add(e.Args);
+ }
+ }
+
+ ///
+ /// Moves buffered scan results into . Always called on the UI thread -
+ /// either from the tick, or from within a Dispatcher.Invoke
+ /// call in / .
+ ///
+ private void FlushResultsBuffer()
+ {
+ List itemsToAdd;
+
+ lock (_resultsBufferLock)
+ {
+ if (_resultsBuffer.Count == 0)
+ return;
+
+ itemsToAdd = [.. _resultsBuffer];
+ _resultsBuffer.Clear();
+ }
+
+ foreach (var item in itemsToAdd)
+ Results.Add(item);
}
private void ProgressChanged(object sender, ProgressChangedArgs e)
{
- PortsScanned = e.Value;
+ Volatile.Write(ref _latestPortsScanned, e.Value);
+ }
+
+ ///
+ /// Pushes the latest buffered progress value into . Always called
+ /// on the UI thread - same calling contexts as .
+ ///
+ private void FlushProgress()
+ {
+ PortsScanned = Volatile.Read(ref _latestPortsScanned);
}
private void ScanComplete(object sender, EventArgs e)
@@ -546,6 +610,10 @@ private void ScanComplete(object sender, EventArgs e)
// to ensure all results are added first #3285
Application.Current.Dispatcher.Invoke(() =>
{
+ _resultsFlushTimer?.Stop();
+ FlushResultsBuffer();
+ FlushProgress();
+
if (Results.Count == 0)
{
StatusMessage = Strings.NoOpenPortsFound;
@@ -559,11 +627,18 @@ private void ScanComplete(object sender, EventArgs e)
private void UserHasCanceled(object sender, EventArgs e)
{
- StatusMessage = Strings.CanceledByUserMessage;
- IsStatusMessageDisplayed = true;
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ _resultsFlushTimer?.Stop();
+ FlushResultsBuffer();
+ FlushProgress();
+
+ StatusMessage = Strings.CanceledByUserMessage;
+ IsStatusMessageDisplayed = true;
- IsCanceling = false;
- IsRunning = false;
+ IsCanceling = false;
+ IsRunning = false;
+ });
}
#endregion
diff --git a/Source/NETworkManager/ViewModels/SettingsGeneralViewModel.cs b/Source/NETworkManager/ViewModels/SettingsGeneralViewModel.cs
index 300ef818ee..ddad5752de 100644
--- a/Source/NETworkManager/ViewModels/SettingsGeneralViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SettingsGeneralViewModel.cs
@@ -45,22 +45,6 @@ public int BackgroundJobInterval
}
}
- public int ThreadPoolAdditionalMinThreads
- {
- get;
- set
- {
- if (value == field)
- return;
-
- if (!_isLoading)
- SettingsManager.Current.General_ThreadPoolAdditionalMinThreads = value;
-
- field = value;
- OnPropertyChanged();
- }
- }
-
public int HistoryListEntries
{
get;
@@ -100,7 +84,6 @@ private void LoadSettings()
}.View;
BackgroundJobInterval = SettingsManager.Current.General_BackgroundJobInterval;
- ThreadPoolAdditionalMinThreads = SettingsManager.Current.General_ThreadPoolAdditionalMinThreads;
HistoryListEntries = SettingsManager.Current.General_HistoryListEntries;
}
diff --git a/Source/NETworkManager/Views/IPScannerSettingsView.xaml b/Source/NETworkManager/Views/IPScannerSettingsView.xaml
index 3d3cc946a3..29b8164223 100644
--- a/Source/NETworkManager/Views/IPScannerSettingsView.xaml
+++ b/Source/NETworkManager/Views/IPScannerSettingsView.xaml
@@ -138,16 +138,5 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Source/NETworkManager/Views/PortScannerSettingsView.xaml b/Source/NETworkManager/Views/PortScannerSettingsView.xaml
index 27077b6ac8..aaf35b0e34 100644
--- a/Source/NETworkManager/Views/PortScannerSettingsView.xaml
+++ b/Source/NETworkManager/Views/PortScannerSettingsView.xaml
@@ -103,16 +103,5 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Source/NETworkManager/Views/SettingsGeneralView.xaml b/Source/NETworkManager/Views/SettingsGeneralView.xaml
index 6eec80259f..4215a2dc17 100644
--- a/Source/NETworkManager/Views/SettingsGeneralView.xaml
+++ b/Source/NETworkManager/Views/SettingsGeneralView.xaml
@@ -186,19 +186,6 @@
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/Website/docs/application/ip-scanner.md b/Website/docs/application/ip-scanner.md
index 25a5f33877..4f41a2f5d7 100644
--- a/Website/docs/application/ip-scanner.md
+++ b/Website/docs/application/ip-scanner.md
@@ -154,7 +154,7 @@ List of TCP ports to scan for each IP address.
**Type:** `String`
-**Default:** `22; 53; 80; 139; 389; 636; 443; 445; 3389`
+**Default:** `22; 53; 80; 135; 139; 389; 636; 443; 445; 3389; 9100`
:::note
@@ -235,7 +235,7 @@ Maximum number of threads used to scan for active hosts (1 thread = 1 host / IP
**Type:** `Integer` [Min `1`, Max `512`]
-**Default:** `256`
+**Default:** `64`
:::warning
@@ -245,19 +245,13 @@ Too many threads can also cause performance problems on the device.
:::
-:::note
-
-This setting only changes the maximum number of concurrently executed threads per host scan. See also the [General](../settings/general#threadpool-additional-min-threads) settings to configure the application-wide thread pool.
-
-:::
-
### Max. concurrent port threads
Maximum number of threads that are used to scan for open ports for each host (IP address).
**Type:** `Integer` [Min `1`, Max `10`]
-**Default:** `5`
+**Default:** `4`
:::warning
@@ -266,9 +260,3 @@ Too many simultaneous requests may be blocked by a firewall. You can reduce the
Too many threads can also cause performance problems on the device.
:::
-
-:::note
-
-This setting only changes the maximum number of concurrently executed threads per port scan. See also the [General](../settings/general#threadpool-additional-min-threads) settings to configure the application-wide thread pool.
-
-:::
diff --git a/Website/docs/application/port-scanner.md b/Website/docs/application/port-scanner.md
index d172fa870a..96d7632aca 100644
--- a/Website/docs/application/port-scanner.md
+++ b/Website/docs/application/port-scanner.md
@@ -142,6 +142,7 @@ List of common TCP ports to scan for.
| SMB | `139; 445` |
| LDAP | `389; 636` |
| HTTP proxy | `3128` |
+| Well-known ports | `1-1024` |
:::note
@@ -181,7 +182,7 @@ Maximum number of threads used to scan hosts (1 thread = 1 host).
**Type:** `Integer` [Min `1`, Max `10`]
-**Default:** `5`
+**Default:** `4`
:::warning
@@ -191,19 +192,13 @@ Too many threads can also cause performance problems on the device.
:::
-:::note
-
-This setting only changes the maximum number of concurrently executed threads per host scan. See also the [General](../settings/general#threadpool-additional-min-threads) settings to configure the application-wide thread pool.
-
-:::
-
### Max. concurrent port threads
Maximum number of threads used to scan for ports for each host (1 thread = 1 port per host).
**Type:** `Integer` [Min `1`, Max `512`]
-**Default:** `256`
+**Default:** `64`
:::warning
@@ -212,9 +207,3 @@ Too many simultaneous requests may be blocked by a firewall. You can reduce the
Too many threads can also cause performance problems on the device.
:::
-
-:::note
-
-This setting only changes the maximum number of concurrently executed threads per port scan. See also the [General](../settings/general#threadpool-additional-min-threads) settings to configure the application-wide thread pool.
-
-:::
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index ff4313b21d..81231a080f 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -19,6 +19,8 @@ Release date: **xx.xx.2026**
## Breaking Changes
+- Removed the **ThreadPool additional min. threads** setting (Settings > General) and the application-wide `ThreadPool.SetMinThreads` workaround it configured for the IP Scanner and Port Scanner. It's no longer needed now that both scan engines use non-blocking async concurrency instead of blocking calls. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
+
## What's new?
- ARM64 builds are now available. [#3538](https://github.com/BornToBeRoot/NETworkManager/issues/3538)
@@ -31,6 +33,18 @@ Release date: **xx.xx.2026**
- 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)
+**IP Scanner**
+
+- 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)
+- Reduced the default **Max. concurrent port threads** from `5` to `4`. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
+- Reduced the default **Max. concurrent host threads** from `256` to `64`, a more conservative default that puts less simultaneous load on the scanned network. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
+
+**Port Scanner**
+
+- Reduced the default **Max. concurrent host threads** from `5` to `4`. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
+- Reduced the default **Max. concurrent port threads** from `256` to `64`, a more conservative default that puts less simultaneous load on the scanned host. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
+- Added a new **Well-known ports** (`1-1024`) default port profile. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
+
## Bug Fixes
**Dashboard**
@@ -39,9 +53,15 @@ 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)
+**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)
+- Fixed the application becoming unresponsive (including the window not reacting to input) during a large scan (e.g. a /24). Scan results and progress were both being pushed to the UI one item/update at a time via a dispatcher call per host/port, which could flood the UI thread's message queue on large scans. Both are now batched and flushed periodically (every 150ms) instead, for **IP Scanner** and **Port Scanner** alike. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
+
## Dependencies, Refactoring & Documentation
- Code cleanup & refactoring
+- Converted the **IP Scanner** and **Port Scanner** engines from `Parallel.ForEach` with blocking calls to genuinely asynchronous `Parallel.ForEachAsync`, removing the risk of exhausting the application's ThreadPool at high concurrency settings. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
- Refactored the **Network Connection** widget's check logic to reduce duplicated code and share the local IP address detection between the Computer and Router checks instead of detecting it twice. [#3553](https://github.com/BornToBeRoot/NETworkManager/pull/3553)
- Consolidated the duplicated **Profiles** side panel (search, tag filter, grouped list, context menu, add/edit/copy/delete) used across 15 tool views into a single shared control, reducing code duplication. As part of this, the profile panel's expanded/width state is now shared across all tools instead of being tracked per tool. [#3537](https://github.com/BornToBeRoot/NETworkManager/pull/3537)
- Language files updated via [#transifex](https://github.com/BornToBeRoot/NETworkManager/pulls?q=author%3Aapp%2Ftransifex-integration)
diff --git a/Website/docs/settings/general.md b/Website/docs/settings/general.md
index 15fcb8c1c6..7b10fb579c 100644
--- a/Website/docs/settings/general.md
+++ b/Website/docs/settings/general.md
@@ -1,7 +1,7 @@
---
sidebar_position: 0
-description: "Configure which applications appear in the NETworkManager sidebar and adjust general application behavior including thread pool settings."
-keywords: [NETworkManager, general settings, sidebar configuration, application settings, thread pool]
+description: "Configure which applications appear in the NETworkManager sidebar and adjust general application behavior."
+keywords: [NETworkManager, general settings, sidebar configuration, application settings]
---
# General
@@ -47,17 +47,3 @@ Maximum number of entries stored in the history for several application inputs.
Type: `Integer`
Default: `5` [Min `0`, Max `25`]
-
-### ThreadPool additional min. threads
-
-Additional [minimum number of threads](https://learn.microsoft.com/en-us/dotnet/api/system.threading.threadpool.setminthreads?view=net-7.0) of the applications [ThreadPool](https://learn.microsoft.com/en-us/dotnet/standard/threading/the-managed-thread-pool) that are created on demand, as new requests are made, before switching to an algorithm for managing thread creation and destruction. This can improve e.g. the IP scanner or port scanner. The value is added to the default settings.
-
-Type: `Integer`
-
-Default: `512` [Min `0`, Max `1024`]
-
-:::note
-
-The value 0 leaves the default settings (number of CPU threads). If the value is to high, performance problems may occur. If the value is higher than the max. threads of the ThreadPool, the max. threads will be used. Changes to this value will take effect after restarting the application. Wheter the value was set successfully can be seen in the log file under `%LocalAppData%\NETworkManager\NETworkManager.log`.
-
-:::