diff --git a/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs b/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
new file mode 100644
index 0000000000..cdcd3a47cd
--- /dev/null
+++ b/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace NETworkManager.Controls;
+
+///
+/// Attached property that converts multiline text (e.g. a column pasted from Excel) into a
+/// semicolon-separated single line when pasted into an editable .
+///
+/// Without this the WPF TextBox inside the ComboBox ( is
+/// false) silently drops everything after the first line when multiline text is pasted.
+/// The conversion is applied by reading the clipboard, replacing the editable text box's
+/// selection with the converted text, and marking as
+/// handled - the system clipboard itself is never modified, so other applications (or a
+/// subsequent paste elsewhere) are unaffected.
+///
+public static class ComboBoxPasteBehavior
+{
+ public static readonly DependencyProperty ConvertMultilineToSemicolonProperty =
+ DependencyProperty.RegisterAttached(
+ "ConvertMultilineToSemicolon",
+ typeof(bool),
+ typeof(ComboBoxPasteBehavior),
+ new PropertyMetadata(false, OnConvertMultilineToSemicolonChanged));
+
+ public static void SetConvertMultilineToSemicolon(UIElement element, bool value) =>
+ element.SetValue(ConvertMultilineToSemicolonProperty, value);
+
+ public static bool GetConvertMultilineToSemicolon(UIElement element) =>
+ (bool)element.GetValue(ConvertMultilineToSemicolonProperty);
+
+ private static void OnConvertMultilineToSemicolonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not ComboBox comboBox)
+ return;
+
+ if ((bool)e.NewValue)
+ CommandManager.AddPreviewExecutedHandler(comboBox, OnPreviewExecuted);
+ else
+ CommandManager.RemovePreviewExecutedHandler(comboBox, OnPreviewExecuted);
+ }
+
+ private static void OnPreviewExecuted(object sender, ExecutedRoutedEventArgs e)
+ {
+ // Keyboard shortcut (Ctrl+V) and the paste context menu item both route through
+ // ApplicationCommands.Paste. Editing commands don't need to be handled here.
+ if (e.Command != ApplicationCommands.Paste)
+ return;
+
+ // The command originates from the focused editable text box inside the ComboBox
+ // template, which is where the pasted text actually needs to be inserted.
+ if (e.OriginalSource is not TextBox textBox)
+ return;
+
+ string text;
+
+ try
+ {
+ if (!Clipboard.ContainsText())
+ return;
+
+ text = Clipboard.GetText();
+ }
+ catch (ExternalException)
+ {
+ // Clipboard is temporarily locked by another process - fall back to the default paste.
+ return;
+ }
+
+ // Only intervene when there is actual multiline content (e.g. a column pasted from
+ // Excel). Otherwise let the default paste command handle it as usual.
+ if (!text.Contains('\n') && !text.Contains('\r'))
+ return;
+
+ var converted = string.Join(";", text
+ .Replace("\r\n", "\n")
+ .Replace('\r', '\n')
+ .Split('\n', StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => x.Trim()));
+
+ // Replace the current selection with the converted text ourselves and mark the command
+ // handled, instead of letting the default paste run - this avoids touching the system
+ // clipboard entirely (no risk of losing other clipboard formats or racing a paste
+ // elsewhere).
+ var selectionStart = textBox.SelectionStart;
+ var textBefore = textBox.Text[..selectionStart];
+ var textAfter = textBox.Text[(selectionStart + textBox.SelectionLength)..];
+
+ textBox.Text = textBefore + converted + textAfter;
+ textBox.SelectionStart = selectionStart + converted.Length;
+ textBox.SelectionLength = 0;
+
+ e.Handled = true;
+ }
+}
diff --git a/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs b/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
index 08636974d6..7c03a29d38 100644
--- a/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
@@ -241,7 +241,7 @@ public static string ExampleHostnameOrIPAddress {
}
///
- /// Looks up a localized string similar to 192.168.178.0/24; 10.0.0.0 - 10.0.0.9; 10.0.[0-9,20].[1-2]; server-01.borntoberoot.net/24.
+ /// Looks up a localized string similar to 192.168.178.0/24; 10.8.0.0-100; 10.8.[0-9,254].1; server-01.borntoberoot.net/28.
///
public static string ExampleHostRange {
get {
diff --git a/Source/NETworkManager.Localization/Resources/StaticStrings.resx b/Source/NETworkManager.Localization/Resources/StaticStrings.resx
index 897bf115a8..b107040973 100644
--- a/Source/NETworkManager.Localization/Resources/StaticStrings.resx
+++ b/Source/NETworkManager.Localization/Resources/StaticStrings.resx
@@ -145,7 +145,7 @@
SERVER-01 or 10.0.0.10
- 192.168.178.0/24; 10.0.0.0 - 10.0.0.9; 10.0.[0-9,20].[1-2]; server-01.borntoberoot.net/24
+ 192.168.178.0/24; 10.8.0.0-100; 10.8.[0-9,254].1; server-01.borntoberoot.net/28
10.0.0.10
diff --git a/Source/NETworkManager.Models/Network/HostRangeHelper.cs b/Source/NETworkManager.Models/Network/HostRangeHelper.cs
index f7b21cadef..be420e8bd2 100644
--- a/Source/NETworkManager.Models/Network/HostRangeHelper.cs
+++ b/Source/NETworkManager.Models/Network/HostRangeHelper.cs
@@ -1,4 +1,5 @@
using NETworkManager.Utilities;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -17,18 +18,40 @@ namespace NETworkManager.Models.Network;
public static class HostRangeHelper
{
///
- /// Create a list of hosts from a string input like "10.0.0.1; example.com; 10.0.0.0/24"
+ /// Create a list of hosts from a string input like "10.0.0.1; example.com; 10.0.0.0/24".
+ /// Inputs can also be separated by newlines (e.g. pasted from Excel, one host/range per line).
///
- /// Hosts like "10.0.0.1; example.com; 10.0.0.0/24"
+ /// Hosts like "10.0.0.1; example.com; 10.0.0.0/24" or newline-separated lines
/// List of hosts.
public static IEnumerable CreateListFromInput(string hosts)
{
- return hosts.Replace(" ", "").Split(';')
- .Where(x => !string.IsNullOrEmpty(x))
+ return hosts.Replace(" ", "")
+ .Split([';', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToArray();
}
+ ///
+ /// Adds every IPv4 address in the inclusive range [, ] to
+ /// . Iterates using the unsigned IPv4 value widened to so the
+ /// inclusive upper bound stays representable even for the last address in the 32-bit space
+ /// (255.255.255.255), which would otherwise overflow arithmetic.
+ ///
+ private static void AddIPv4RangeToBag(IPAddress start, IPAddress end,
+ ConcurrentBag<(IPAddress ipAddress, string hostname)> hostsBag, CancellationToken ct)
+ {
+ var from = (long)unchecked((uint)IPv4Address.ToInt32(start));
+ var to = (long)unchecked((uint)IPv4Address.ToInt32(end));
+
+ Parallel.For(from, to + 1, (i, state) =>
+ {
+ if (ct.IsCancellationRequested)
+ state.Break();
+
+ hostsBag.Add((IPv4Address.FromInt32(unchecked((int)i)), string.Empty));
+ });
+ }
+
public static async Task<(List<(IPAddress ipAddress, string hostname)> hosts, List hostnamesNotResolved)>
ResolveAsync(IEnumerable hosts, bool dnsResolveHostnamePreferIPv4, CancellationToken cancellationToken)
{
@@ -54,14 +77,17 @@ public static IEnumerable CreateListFromInput(string hosts)
case var _ when RegexHelper.IPv4AddressSubnetmaskRegex().IsMatch(host):
var network = IPNetwork2.Parse(host);
- Parallel.For(IPv4Address.ToInt32(network.Network), IPv4Address.ToInt32(network.Broadcast) + 1,
- (i, state) =>
- {
- if (ct.IsCancellationRequested)
- state.Break();
+ AddIPv4RangeToBag(network.Network, network.Broadcast, hostsBag, ct);
- hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
- });
+ break;
+
+ // 192.168.0.1-100
+ case var _ when RegexHelper.IPv4AddressShortRangeRegex().IsMatch(host):
+ var shortRange = host.Split('-');
+ var shortBase = shortRange[0][..shortRange[0].LastIndexOf('.')];
+
+ AddIPv4RangeToBag(IPAddress.Parse(shortRange[0]),
+ IPAddress.Parse($"{shortBase}.{shortRange[1]}"), hostsBag, ct);
break;
@@ -69,14 +95,7 @@ public static IEnumerable CreateListFromInput(string hosts)
case var _ when RegexHelper.IPv4AddressRangeRegex().IsMatch(host):
var range = host.Split('-');
- Parallel.For(IPv4Address.ToInt32(IPAddress.Parse(range[0])),
- IPv4Address.ToInt32(IPAddress.Parse(range[1])) + 1, (i, state) =>
- {
- if (ct.IsCancellationRequested)
- state.Break();
-
- hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
- });
+ AddIPv4RangeToBag(IPAddress.Parse(range[0]), IPAddress.Parse(range[1]), hostsBag, ct);
break;
@@ -170,14 +189,7 @@ public static IEnumerable CreateListFromInput(string hosts)
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));
- });
+ AddIPv4RangeToBag(network.Network, network.Broadcast, hostsBag, ct);
}
else
{
diff --git a/Source/NETworkManager.Utilities/RegexHelper.cs b/Source/NETworkManager.Utilities/RegexHelper.cs
index 0785fc2fa6..8462c2eedf 100644
--- a/Source/NETworkManager.Utilities/RegexHelper.cs
+++ b/Source/NETworkManager.Utilities/RegexHelper.cs
@@ -43,14 +43,30 @@ public static partial class RegexHelper
public static partial Regex IPv4AddressExtractRegex();
///
- /// Provides a compiles regular expression that matches IPv4 address ranges in the format "start-end" like
+ /// Represents a regular expression pattern that matches valid shorthand IPv4 address ranges like
+ /// "192.168.178.1-100" (base IP + last octet range).
+ ///
+ private const string IPv4AddressShortRangeValues =
+ @"((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\-(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
+
+ ///
+ /// Provides a compiled regular expression that matches IPv4 address ranges in the format "start-end" like
/// "192.168.178.0-192.168.178.255".
- ///
+ ///
/// A instance that matches strings representing IPv4 address ranges, such as
/// "192.168.1.1-192.168.1.100".
[GeneratedRegex($"^{IPv4AddressValues}-{IPv4AddressValues}$")]
public static partial Regex IPv4AddressRangeRegex();
+ ///
+ /// Provides a compiled regular expression that matches shorthand IPv4 address ranges like
+ /// "192.168.178.1-100" (base IP followed by a last-octet range).
+ ///
+ /// A instance that matches strings representing shorthand IPv4 address ranges,
+ /// such as "192.168.1.1-100" (192.168.1.1 to 192.168.1.100).
+ [GeneratedRegex($"^{IPv4AddressShortRangeValues}$")]
+ public static partial Regex IPv4AddressShortRangeRegex();
+
///
/// Provides a compiled regular expression that matches valid IPv4 subnet mask like "255.255.0.0".
///
diff --git a/Source/NETworkManager.Validators/MultipleHostsRangeValidator.cs b/Source/NETworkManager.Validators/MultipleHostsRangeValidator.cs
index a2674ce8ae..7f79762569 100644
--- a/Source/NETworkManager.Validators/MultipleHostsRangeValidator.cs
+++ b/Source/NETworkManager.Validators/MultipleHostsRangeValidator.cs
@@ -1,6 +1,7 @@
using NETworkManager.Localization.Resources;
using NETworkManager.Models.Network;
using NETworkManager.Utilities;
+using System;
using System.DirectoryServices.ActiveDirectory;
using System.Globalization;
using System.Net;
@@ -18,7 +19,9 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
if (value == null)
return new ValidationResult(false, Strings.EnterValidIPScanRange);
- foreach (var ipHostOrRange in ((string)value).Replace(" ", "").Split(';'))
+ foreach (var ipHostOrRange in ((string)value)
+ .Replace(" ", "")
+ .Split([';', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
{
// 192.168.0.1
if (RegexHelper.IPv4AddressRegex().IsMatch(ipHostOrRange))
@@ -32,6 +35,19 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
if (RegexHelper.IPv4AddressSubnetmaskRegex().IsMatch(ipHostOrRange))
continue;
+ // 192.168.0.1-100
+ if (RegexHelper.IPv4AddressShortRangeRegex().IsMatch(ipHostOrRange))
+ {
+ var shortRange = ipHostOrRange.Split('-');
+ var shortBase = shortRange[0][..shortRange[0].LastIndexOf('.')];
+
+ if (IPv4Address.ToInt32(IPAddress.Parse(shortRange[0])) >
+ IPv4Address.ToInt32(IPAddress.Parse($"{shortBase}.{shortRange[1]}")))
+ isValid = false;
+
+ continue;
+ }
+
// 192.168.0.0 - 192.168.0.100
if (RegexHelper.IPv4AddressRangeRegex().IsMatch(ipHostOrRange))
{
diff --git a/Source/NETworkManager/Views/IPScannerView.xaml b/Source/NETworkManager/Views/IPScannerView.xaml
index c60cf86ed4..0a85dce0ba 100644
--- a/Source/NETworkManager/Views/IPScannerView.xaml
+++ b/Source/NETworkManager/Views/IPScannerView.xaml
@@ -68,6 +68,7 @@
ItemsSource="{Binding Path=HostHistoryView}"
mah:TextBoxHelper.Watermark="{x:Static Member=localization:StaticStrings.ExampleHostRange}"
IsReadOnly="{Binding Path=IsRunning}"
+ controls:ComboBoxPasteBehavior.ConvertMultilineToSemicolon="True"
Style="{StaticResource ResourceKey=HistoryComboBox}">
diff --git a/Source/NETworkManager/Views/PingMonitorHostView.xaml b/Source/NETworkManager/Views/PingMonitorHostView.xaml
index 0d11f8f732..8971374365 100644
--- a/Source/NETworkManager/Views/PingMonitorHostView.xaml
+++ b/Source/NETworkManager/Views/PingMonitorHostView.xaml
@@ -97,6 +97,7 @@
ItemsSource="{Binding Path=HostHistoryView}"
mah:TextBoxHelper.Watermark="{x:Static Member=localization:StaticStrings.ExampleHostRange}"
IsReadOnly="{Binding Path=IsRunning}"
+ controls:ComboBoxPasteBehavior.ConvertMultilineToSemicolon="True"
Style="{StaticResource ResourceKey=HistoryComboBox}">
diff --git a/Source/NETworkManager/Views/PortScannerView.xaml b/Source/NETworkManager/Views/PortScannerView.xaml
index 54bb2151fa..bbf3f36012 100644
--- a/Source/NETworkManager/Views/PortScannerView.xaml
+++ b/Source/NETworkManager/Views/PortScannerView.xaml
@@ -60,6 +60,7 @@
ItemsSource="{Binding HostHistoryView}"
mah:TextBoxHelper.Watermark="{x:Static localization:StaticStrings.ExampleHostRange}"
IsReadOnly="{Binding Path=IsRunning}"
+ controls:ComboBoxPasteBehavior.ConvertMultilineToSemicolon="True"
Style="{StaticResource HistoryComboBox}">
@@ -79,6 +80,7 @@
ItemsSource="{Binding PortsHistoryView}"
mah:TextBoxHelper.Watermark="{x:Static localization:StaticStrings.ExamplePortScanRange}"
IsReadOnly="{Binding Path=IsRunning}"
+ controls:ComboBoxPasteBehavior.ConvertMultilineToSemicolon="True"
Style="{StaticResource HistoryComboBox}">
diff --git a/Website/docs/application/ip-scanner.md b/Website/docs/application/ip-scanner.md
index 4f41a2f5d7..6c46b528d4 100644
--- a/Website/docs/application/ip-scanner.md
+++ b/Website/docs/application/ip-scanner.md
@@ -25,6 +25,7 @@ With the **IP Scanner** you can scan for active devices based on the hostname or
| -------------------------------- | -------------------------------------------------------------------------------------------- |
| `10.0.0.1` | Single IP address (`10.0.0.1`) |
| `10.0.0.100 - 10.0.0.199` | All IP addresses in a given range (`10.0.0.100`, `10.0.0.101`, ..., `10.0.0.199`) |
+| `10.0.0.100-199` | All IP addresses in a given range, shorthand for the last octet (`10.0.0.100`, ..., `10.0.0.199`) |
| `10.0.0.0/23` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.0.0/255.255.254.0` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.[0-9,20].[1-2]` | Multiple IP addresses like (`10.0.0.1`, `10.0.0.2`, `10.0.1.1`, ...,`10.0.9.2`, `10.0.20.1`) |
@@ -34,9 +35,9 @@ With the **IP Scanner** you can scan for active devices based on the hostname or
:::note
-Multiple inputs can be combined with a semicolon (`;`).
+Multiple inputs can be combined with a semicolon (`;`). A column pasted from Excel (one entry per line) is converted to the semicolon-separated form automatically.
-Example: `10.0.0.0/24; 10.0.[10-20]1`
+Example: `10.0.0.0/24; 10.0.[10-20].1; 10.0.0.100-199`
:::
diff --git a/Website/docs/application/ping-monitor.md b/Website/docs/application/ping-monitor.md
index a1ad0a605d..08f2cc4a4b 100644
--- a/Website/docs/application/ping-monitor.md
+++ b/Website/docs/application/ping-monitor.md
@@ -22,6 +22,7 @@ ICMP (Internet Control Message Protocol) is a network-layer protocol used to sen
| -------------------------------- | ------------------------------------------------------------------------------------------ |
| `10.0.0.1` | Single IP address (`10.0.0.1`) |
| `10.0.0.100 - 10.0.0.199` | All IP addresses in a given range (`10.0.0.100`, `10.0.0.101`, ..., `10.0.0.199`) |
+| `10.0.0.100-199` | All IP addresses in a given range, shorthand for the last octet (`10.0.0.100`, ..., `10.0.0.199`) |
| `10.0.0.0/23` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.0.0/255.255.254.0` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.[0-9,20].[1-2]` | Multiple IP addresses like (`10.0.0.1`, `10.0.0.2`, `10.0.1.1`, ...,`10.0.9.2`, `10.0.20.1`) |
@@ -31,9 +32,9 @@ ICMP (Internet Control Message Protocol) is a network-layer protocol used to sen
:::note
-Multiple inputs can be combined with a semicolon (`;`).
+Multiple inputs can be combined with a semicolon (`;`). A column pasted from Excel (one entry per line) is converted to the semicolon-separated form automatically.
-Example: `10.0.0.0/24; 10.0.[10-20]1`
+Example: `10.0.0.0/24; 10.0.[10-20].1; 10.0.0.100-199`
:::
diff --git a/Website/docs/application/port-scanner.md b/Website/docs/application/port-scanner.md
index 96d7632aca..cdcbd94457 100644
--- a/Website/docs/application/port-scanner.md
+++ b/Website/docs/application/port-scanner.md
@@ -31,6 +31,7 @@ TCP (Transmission Control Protocol) is a connection-oriented transport-layer pro
| -------------------------------- | -------------------------------------------------------------------------------------------- |
| `10.0.0.1` | Single IP address (`10.0.0.1`) |
| `10.0.0.100 - 10.0.0.199` | All IP addresses in a given range (`10.0.0.100`, `10.0.0.101`, ..., `10.0.0.199`) |
+| `10.0.0.100-199` | All IP addresses in a given range, shorthand for the last octet (`10.0.0.100`, ..., `10.0.0.199`) |
| `10.0.0.0/23` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.0.0/255.255.254.0` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.[0-9,20].[1-2]` | Multiple IP addresses like (`10.0.0.1`, `10.0.0.2`, `10.0.1.1`, ...,`10.0.9.2`, `10.0.20.1`) |
@@ -45,9 +46,9 @@ TCP (Transmission Control Protocol) is a connection-oriented transport-layer pro
:::note
-Multiple inputs can be combined with a semicolon (`;`).
+Multiple inputs can be combined with a semicolon (`;`). A column pasted from Excel (one entry per line) is converted to the semicolon-separated form automatically.
-Example: `10.0.0.0/24; 10.0.[10-20]1` or `1-1024; 8080; 8443`
+Example: `10.0.0.0/24; 10.0.[10-20].1; 10.0.0.100-199` or `1-1024; 8080; 8443`
:::
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index 81231a080f..13ea8a10a3 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -35,12 +35,21 @@ Release date: **xx.xx.2026**
**IP Scanner**
+- 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)
- 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)
+**Ping Monitor**
+
+- Host input now accepts newline-separated hosts (one per line, e.g. a column pasted from Excel), converted automatically to the semicolon-separated form. [#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)
+
**Port Scanner**
+- Host and Ports input fields now accept newline-separated entries (one per line, e.g. a column pasted from Excel), converted automatically to the semicolon-separated form. [#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)
- 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)