Repository navigation
Feature: IP Scanner / Port Scanner: bug fixes, faster and more reliable scans - #3653
Merged
Merged
Conversation
- 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>
- 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>
.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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
…, 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>
… 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>
…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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
…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>
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>
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>
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>
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>
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>
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>
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>
…) (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>
Contributor
|
This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Review of the IP Scanner and Port Scanner (bugs, behaviour, performance), fixed one commit per finding.
Bug fixes (IP Scanner + Port Scanner)
TIME_WAITsockets and exhaust the local ports.SIO_TCP_INITIAL_RTO, RTO = timeout); new Attempts setting (default 1) to retry ports without answer on unreliable networks.10.0.0.1; 10.0.0.0/24, also Ping Monitor), different hostnames of one IP are kept; ports are scanned once (80; 1-100)./1).128.0.0.0are accepted by the validator.IP Scanner
{{Hostname}}(from DNS/NetBIOS, i.e. from the network) is only used if it's a valid hostname (command injection, e.g.&withcmd.exe /c); Edit/Delete need a selection.Performance
SocketAsyncEventArgs) - the app no longer lags while scanning with a debugger attached.Docs: settings (defaults, ranges, attempts, threads/firewall note), context menu, result, keyboard shortcuts.
To-Do
Contributing
By submitting this pull request, I confirm the following: