Repository navigation
Commit d4e2141
Feature: IP Scanner / Port Scanner: bug fixes, faster and more reliable scans (#3653)
* Fix: IP Scanner / Port Scanner single completion path (B2, B3, B4, B16)
- The model raises ScanComplete exactly once with ScanCompleteArgs
(IsCanceled, Exception); UserHasCanceled is removed.
- The token is no longer passed to Task.Run, so a cancel right before the
scan starts can't skip the completion event (tab stuck).
- Unexpected exceptions (also GetNetworkInterfaces) are reported instead of
being swallowed as "nothing found" and are logged in the view model.
- The view model resets its state in one place and unsubscribes the
scanner's handlers; "Canceled by user" is no longer overwritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: IP Scanner / Port Scanner robust scan preparation (B5, B8, B11)
- Catch every exception while the hosts are resolved and the ports are
converted (Start() is fire-and-forget) and end the scan via the single
completion path, so the tab can't get stuck.
- Append "No reachable hosts found" / "No open ports found" to a message
from the preparation (unresolved hostnames) instead of replacing it.
- Detect subnet: try/catch/finally, so the button can't stay disabled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Limit ICMP buffer to 65500 bytes (B4)
.NET's Ping throws an ArgumentException for buffers larger than 65500
bytes, which made every host unreachable in the IP Scanner (and broke
Ping Monitor / Traceroute). The settings allowed up to 65535.
- Maximum 65500 in the IP Scanner, Ping Monitor and Traceroute settings
- Lower stored values above 65500 on upgrade
- Docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: IP Scanner ICMP attempts ignored (B1)
PingAsync returned on the first non-success reply, so a single lost echo
marked the host as unreachable. Try again while the request timed out
(up to the configured attempts); other states (e.g. destination host
unreachable) are final. Cancellation is handled by SendPingAsync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Port probe classification and TIME_WAIT (B17, B18)
- Only a refused connection (TCP reset) is "Closed"; other connect errors
(host/network unreachable, local socket errors) mean no answer and are
"TimedOut" now. Before, every error was "Closed".
- Close with SO_LINGER 0 (reset), so open ports no longer leave sockets in
TIME_WAIT that can exhaust the ephemeral ports on large scans.
Shared by the IP Scanner and Port Scanner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Feature: IP Scanner detects hosts by a refused port (B17, D7)
A closed port (connection refused / TCP reset) is an answer from the host,
so the host is reachable even if ICMP is blocked and no port is open (same
as nmap's host discovery). The port scan returns every port now; closed
and timed out ports are only shown with "Show unreachable".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Chore: New IP Scanner / Port Scanner defaults (P4, PS4, D1, D5)
The time per host was dominated by the port probes: 11 default ports with
4 port threads = 3 rounds x 4000 ms = ~12 s for every host with filtered
ports (all unreachable hosts), an empty /24 took ~48 s.
IP Scanner:
- ICMP / port / NetBIOS timeout 4000 -> 2000 ms (Windows' initial SYN RTO
is 1 s, so 2 s still allows one retransmission)
- Max. concurrent port threads 4 -> 16 (all default ports at once), max 32
- Max. concurrent host threads 64 -> 128, max 512 -> 256
(128 x 11 = ~1.4k connections in flight, worst case 256 x 32 = 8k, half
of the ephemeral port range)
Port Scanner:
- Timeout 4000 -> 2000 ms, Max. concurrent port threads 64 -> 128
Migration in UpgradeToLatest replaces the existing block: values still on
the default of the last release (2026.7.7) or of a pre-release of this
release are updated, IP Scanner host threads above 256 are lowered.
Docs explain what a "thread" is and that a firewall may block too many
simultaneous connections.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: De-duplicate and expand host ranges faster (B7, B20, P7, P8)
HostRangeHelper (IP Scanner, Port Scanner, Ping Monitor):
- Remove duplicate IP addresses of overlapping inputs, keep the entry with
a hostname
- Expand ranges sequentially into a pre-sized list instead of Parallel.For
into a ConcurrentBag; the result of a single range is already sorted, so
the sort is skipped after one linear check
- Check for cancellation while expanding (Parallel.For's state.Break()
didn't stop it)
IPAddressHelper.CompareIPAddresses no longer allocates two arrays per call
(stackalloc + TryWriteBytes), used by every sorted IP list in the app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Confirm large scans and refuse too large ones (B6)
A /1 (allowed by the input validation) created 2^31 entries and ran out of
memory. HostRangeHelper.GetHostCount counts the inputs without expanding
or resolving them (hostname = 1), the scan is then
- confirmed above 65,536 IP addresses (Port Scanner: hosts x ports)
- refused above 16,777,216 (a /8) with a status message
before any state is changed. Limits in GlobalStaticConfiguration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Scan ports only once (B19)
PortRangeHelper.ConvertPortRangeToIntArray removes duplicates (first
occurrence, order kept), e.g. "80; 1-100" or multiple selected port
profiles scanned port 80 twice (two rows, counted twice).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Validate IP ranges across 128.0.0.0 (B14)
MultipleHostsRangeValidator (IP Scanner, Port Scanner, Ping Monitor,
profiles, run command) compared the signed IPv4 value, so a range like
127.255.255.250 - 128.0.0.5 was rejected as "start > end". Compare the
unsigned value like HostRangeHelper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Port Scanner sort by IP address crash, sorted results (PS1-PS3)
- Sorting the IP address column used the built-in sort on IPAddress,
which isn't IComparable -> exception as soon as the results contained
more than one host. Sort with IPAddressHelper.CompareIPAddresses (+ port)
in a Sorting handler like the IP Scanner.
- Results are sorted by IP address and port by default instead of the
order in which the ports finished.
- The PTR lookup runs while the ports are scanned (each result awaits the
hostname, which is needed for the grouping) instead of before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: IP Scanner NetBIOS column sort/visibility and Copy menu (B9, B10, C6)
- "NetBIOS reachable" column: SortMemberPath pointed to a property that
doesn't exist (no sort), now NetBIOSInfo.IsReachable; hidden with NetBIOS
disabled like the row details.
- Copy > Hostname / MAC address / Vendor use the same visibility as their
columns (DNS / MAC resolution OR NetBIOS).
- Copy > Status copies "Up" / "Down" (new HostReachableToStringConverter)
instead of True / False.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: IP Scanner custom commands - hostname injection, CanExecute (S1, B15, T2)
- {{Hostname}} comes from the network (PTR / NetBIOS name). A name with
e.g. "&" could run additional commands with "cmd.exe /c ...". Only a
valid hostname (RegexHelper.HostnameOrDomainRegex) is used, otherwise an
empty value + log warning. Docs note.
- Settings: Edit / Delete need a selected command (F2 / Del without a
selection opened an empty dialog / asked to delete nothing); the delete
confirmation names the command.
- Log a warning if a custom command no longer exists (old ToDo).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Improvement: IP Scanner MAC lookup from a cached neighbor table (P1, B12, D2)
- NeighborTable.GetMACAddressMap() reads the table once into a map
(IPv6 without scope id); it replaces GetMACAddress, which read and
marshalled the whole table for every reachable host.
- The IP Scanner caches the map per scan and only reads it again on a miss
if the snapshot is older than the time the host answered. Before a host
can answer, its ARP/NDP entry exists, so a host is always looked up in a
table read after its answer; unreachable hosts never trigger a read.
- Local MAC: also the own IPv6 (incl. link-local) addresses; link-local
neighbors entered without scope id are found.
- Resolve MAC address and vendor: enabled by default for new installations
(documented as enabled, the code default was false).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Improvement: IP Scanner / Port Scanner export in the background (P5)
Copy the results (all or selected) on the UI thread - they may still grow
while a scan runs - and run ExportManager.Export with Task.Run, so a large
export (e.g. a /16 with "show unreachable") doesn't freeze the window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: IP Scanner / Port Scanner context menu, result and shortcuts (D-1)
- IP Scanner: complete context menu table (open in other tools, DNS lookup
/ RDAP sub items, custom commands, Add profile...), multi selection
- Result section: up/down or open/closed summary, row details, which
results are shown, status messages, port states (open / closed / timed out)
- Keyboard shortcuts: Enter, Esc, Ctrl+T
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Handle errors while counting the hosts / ports (B5, B6)
The size check added for B6 runs before the preparation's try/catch, so an
unexpected parse error would have been lost in the fire-and-forget Start().
Show it as status message and log it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Improvement: Stagger the start of hosts / ports in the IP / Port Scanner
All hosts of a "wave" started at the same moment and, because of the same
timeouts, also finished at the same moment: ~1400 connection requests within
a few milliseconds (128 hosts x 11 ports) and a progress that moved in large
jumps. StartScheduler gives every host / port the next free start time
(one per interval) and only waits until then, so the waits don't add up and
there is no delay once all concurrent slots are busy.
- IP Scanner: one host every 10 ms (128 hosts ramp up in ~1.3 s, empty /24
~9.3 s instead of ~8 s)
- Port Scanner: one port every 1 ms across all hosts (a larger interval
would limit the throughput of 4 x 128 ports)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refactor: Exception-free port probe and NetBIOS request
Nearly every probe of a scan ended with a caught exception: a port timeout
(OperationCanceledException), a refused / unreachable connection
(SocketException) and every host without a NetBIOS answer - ~12 per host,
~3000 for a /24. Cheap without a debugger, but with Visual Studio attached
every first-chance exception pauses the process, so the UI lagged while
scanning.
- SocketOperation: runs a SocketAsyncEventArgs operation and returns its
SocketError as Task result (shared by PortProbe and NetBIOSResolver)
- PortProbe: Socket.ConnectAsync(e); timeout / cancellation abort it with
Socket.CancelConnectAsync (completes with OperationAborted). Same states
as before (Success -> Open, ConnectionRefused -> Closed, else TimedOut),
only a cancellation by the caller still throws.
- NetBIOSResolver: SendToAsync / ReceiveFromAsync(e); timeout / cancellation
close the socket. The response is parsed from the received bytes only.
Tested on Linux (open / refused / dropped / IPv6 / 400 parallel probes,
NetBIOS with a fake responder, no answer, cancellation): no first-chance
exceptions except for the caller's cancellation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Don't start a confirmed large scan in a closed tab (B6)
The confirmation for a large scan is shown before IsRunning is set, so
closing the tab while the dialog was open didn't stop anything (OnClose only
stops a running scan). Confirming afterwards started the scan in the closed
tab, where it kept running in the background and was never canceled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refactor: IPv4Address.ToUInt32 for unsigned IPv4 values (review #8)
The unsigned conversion unchecked((uint)IPv4Address.ToInt32(...)) was
written out in HostRangeHelper (twice) and the range validator. B14 came
from comparing the signed value - one helper makes it the obvious choice
for the next caller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: IP Scanner MAC lookup only for hosts in a local subnet (review #5)
A host behind a router is never in the neighbor table (it contains the
router), so every reachable remote host missed the cached snapshot and read
the whole table again - one at a time under a lock, while up to 128 host
workers blocked thread-pool threads waiting for it.
- Look up the neighbor table only for hosts in a local subnet: IPv4 via the
subnet mask of the local interfaces, IPv6 link-local or the same /64 as a
local address. Remote hosts use the NetBIOS MAC address (as before, the
neighbor table never had their MAC).
- SemaphoreSlim.WaitAsync instead of lock, so waiting hosts don't block a
thread while another one reads the table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Start interval must not limit fast scans (review #2)
The fixed interval (IP Scanner 10 ms, Port Scanner 1 ms) was a hard cap of
100 hosts / 1000 ports per second. With short timeouts and many threads the
scan can finish more (e.g. 256 host threads, 1 x 500 ms ICMP: ~500 hosts/s).
StartScheduler.GetInterval: the interval is at most duration / concurrency
(a longer one starts fewer operations per second than can finish).
- IP Scanner: duration = slowest of ICMP attempts x timeout, port scan
(timeout x rounds) and NetBIOS timeout; defaults -> 10 ms as before
- Port Scanner: duration = timeout, concurrency = host x port threads;
defaults -> 1 ms as before
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Keep different hostnames of an IP and the IPv6 scope id when de-duplicating (review #1, #3)
SortAndRemoveDuplicates merged all entries with the same IP address into one.
Two hostnames of one IP (e.g. web1/web2 of a load balancer) became one
entry - in Ping Monitor one monitor instead of two - and because List.Sort
is unstable, which one survived could change from run to run. The compare
also ignored the IPv6 scope id, so fe80::1%3 and fe80::1%7 (two devices on
different interfaces) were merged.
- Every different hostname (case-insensitive) is kept once; an entry
without hostname only if there is none with a hostname (B7 unchanged:
"10.0.0.1; 10.0.0.0/24" and "server; <its IP>" give one entry)
- Stable sort (OrderBy), entries with the same IP keep the input order
- Same bytes but different scope id = different hosts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Export the results in the order shown in the grid (review #10)
Both scanners exported the Results collection, which is in the order the
hosts / ports were finished, while the grid shows them sorted (Port Scanner
since PS2, IP Scanner by IP address or the user's sort).
- IP Scanner: export ResultsView (sorted)
- Port Scanner: export group by group (grouped by host, sorted within the
group) - like the grid also when the user sorts by another column
- Selected results: in the same order instead of the selection order
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: Note on port timeouts below 1000 ms (review #4)
Windows retries a refused connect and reports the reset only after about
1 s, so with a port timeout below ~1000 ms closed ports become "timed out"
and the IP Scanner can't detect a host by a closed port. The default
(2000 ms) is not affected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Report closed ports immediately on Windows (review #4)
Windows retries the connection request (SYN) after a reset like after a
lost request and reports ConnectionRefused only after ~2 s (measured:
2013-2075 ms). That's as long as the new default port timeout (2000 ms),
so closed ports were shown as timed out and the IP Scanner couldn't detect
a host by a closed port; with the old default every closed port blocked a
slot for 2 s.
SIO_TCP_INITIAL_RTO with TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS (measured:
ConnectionRefused after 0 ms) and the probe timeout as initial RTO: the
request is sent once, a reset is reported immediately, and Windows waits
for an answer as long as the probe does. Ignored if not supported.
Docs: replace the note from 7e9c5b6 (it said ~1 s and < 1000 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Only stagger the ramp-up of a scan (review #1, #2, #5)
The StartScheduler reserved a start time for every operation, so it capped
a scan at one start per interval also after the ramp-up: the IP Scanner at
100 hosts/s, the Port Scanner at 1000 probes/s. Since closed ports are
reported immediately, a host refusing most ports took >= 65 s for 1-65535.
Only the first N starts (N = concurrency: min(hosts, host threads), for the
Port Scanner x min(ports, port threads)) are spread. After that every slot
is busy and an operation starts when another one has finished - spread like
the ramp-up, without delay.
Simulated (Task.Delay as work): 256 hosts x 4 s unchanged (9.3 s), 1024
hosts x 20 ms 1.3 s instead of >= 10.2 s, 65535 ports x 2 ms 2.1 s instead
of >= 65 s. Comments also mention the ~15.6 ms timer resolution of
Task.Delay (sub-tick intervals start a few operations per tick).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: NetBIOS response without MAC address (review #6)
The length check only covered the name table, but ExtractNames always read
the 6 MAC bytes after it. A response without them (no statistics block)
threw IndexOutOfRangeException and the catch dropped the names that were
already parsed. Read the MAC address only if the response contains it (and
don't format an empty one, GetDefaultFormat would throw as well).
Tested with a fake responder: with MAC -> names + MAC, without -> names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Only accept the NetBIOS answer of the scanned host (review #7)
ReceiveFromAsync accepted the first datagram from any sender, so a stray
or spoofed reply to the ephemeral port could be credited to the scanned
host (wrong computer name / MAC, marked reachable). Ignore datagrams from
another address and keep waiting until the timeout.
Tested: reply only from another address -> no answer (timeout), stray
reply first and the real one 100 ms later -> real one is used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix: Log when the SYN retransmissions can't be disabled (review #8)
If SIO_TCP_INITIAL_RTO fails (should not happen since Windows 10 1703, but
e.g. after a wrong refactoring of the buffer layout), closed ports silently
went back to being reported after ~2 s. Log a warning once per application
run (it would fail for every probe).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Feature: Attempts setting for the port scan (IP Scanner, Port Scanner) (review #3)
Since the SYN retransmissions are disabled (closed ports are reported
immediately), a lost connection request makes an open port "timed out".
Like the ICMP Attempts: another connection request is sent if there was no
answer (timed out); open and closed ports are final, so only ports without
answer take attempts x timeout.
- Settings: IPScanner_PortScanAttempts, PortScanner_Attempts (default 1,
1-10), next to the port timeout in both settings pages
- PortProbe.ProbeAsync(..., attempts, ...) loops over ProbeOnceAsync
- The start interval / host duration include the attempts
- Docs: tip for LAN (1) vs Wi-Fi / VPN / internet (2)
Tested: dropped port 300 ms x 1/2/3 -> 317/601/901 ms TimedOut, refused
with 3 attempts -> Closed after 1 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Chore: Add PR
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>1 parent fdc441f commit d4e2141
38 files changed
Lines changed: 1676 additions & 398 deletions
File tree
- Source
- NETworkManager.Converters
- NETworkManager.Localization/Resources
- NETworkManager.Models/Network
- NETworkManager.Settings
- NETworkManager.Utilities
- NETworkManager.Validators
- NETworkManager
- ViewModels
- Views
- Website/docs
- application
- changelog
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5004 | 5004 | | |
5005 | 5005 | | |
5006 | 5006 | | |
| 5007 | + | |
| 5008 | + | |
| 5009 | + | |
| 5010 | + | |
| 5011 | + | |
| 5012 | + | |
| 5013 | + | |
| 5014 | + | |
| 5015 | + | |
| 5016 | + | |
| 5017 | + | |
| 5018 | + | |
| 5019 | + | |
| 5020 | + | |
| 5021 | + | |
| 5022 | + | |
5007 | 5023 | | |
0 commit comments