Skip to content

Port Scan Detection Limits

NRGLine4Sec edited this page Oct 17, 2026 · 1 revision

Port scan detection: limits

Back to Port Scan Detection.

Everything on this page is measured. The technique-by-technique table and the timing figures are generated from the engine and checked by tests: see Port Scan Detection: Coverage. The other figures are measurements made on the engine or on real scans, and say how.

What it does not detect

The full table, generated by replaying real captures through the engine, is in Port Scan Detection: Coverage. The honest summary:

  • A scan slower than the decay rate. With the defaults, a SYN scan of system ports is detected up to about one probe every 1 m 36 s; nmap -T0 waits five minutes and is invisible. This is a property of any leaky-bucket detector, and the exact figure for your configuration is in the coverage report.

    That figure carries a horizon, and it has to. Any interval below weight x DecayInterval makes the score grow eventually, but the time to reach the threshold diverges as that product is approached: at one probe every 1 m 50 s the threshold is crossed after about 3 h 25, by which point the scanner has finished and left. So the number quoted here and in the coverage report is the slowest scan reported within one hour, which is the only form of the answer that means anything to a reader.

  • A scan that pauses long enough for the bucket to empty. The score leaks one point per DecayInterval, so a scanner that sends eight probes and then waits about sixteen minutes starts again from zero every time. Measured at the defaults: 1040 ports covered in under 35 hours (eight probes, then sixteen minutes of silence, 130 times) without a single alert; with fifteen-minute pauses, every burst is reported. This is the point above seen from the other side, and it is what a leaky bucket buys -- it is also why the bucket is a bucket and not a fixed window, which would miss far more. Raising PortScan.DecayInterval lengthens the pause an attacker needs, at the cost of holding stale score for longer.

  • A scan spread across many source addresses. The score is per source, so an attacker who can present enough distinct addresses never accumulates enough on any one of them. Measured at the defaults: 128 addresses cover ports 1-1023, and 3633 addresses cover all 65535 ports in 65 seconds, with no alert at any point. On IPv6 that costs nothing at all -- a single /64 supplies as many addresses as anyone could want.

    Detecting this needs correlation between sources -- many addresses each touching a few ports of one host in a short window -- which is a different detector from the one described here, not a threshold that could be tuned. It is not implemented, and calling the per-source score a defence against it would be false.

  • Who is really scanning you, whenever indirection is used. With -D decoys, -S spoofing, -sI idle scan or -b FTP bounce, the scan is fully visible and the origin is not recoverable from the packets. The address you see belongs to someone else.

  • nmap -sW as distinct from -sA. They are byte-identical on the wire; the difference is entirely in how the scanner reads the reply. It is reported as an ACK scan, which is what actually arrived.

  • The technique behind nmap -f. Fragmentation puts the TCP flags outside the first fragment. The ports are there, so the scan is detected, but the flags were never read, and reporting the zero byte as a NULL scan would be a fabrication.

  • A scan hidden behind a flood of forged source addresses. The source table is bounded, so anyone able to present more distinct addresses than it holds can displace what is in it, including the scan's own accumulated state, which then never crosses the threshold. Eviction discards the weakest of the least recently used, which sets the price at about one full table of forged packets per hidden probe -- measured, not estimated -- but no eviction policy removes the possibility: refusing to admit new sources instead would let one filling of the table stop every later scanner from being tracked at all. What the daemon does instead is say when the price is being paid: a source table turning over more than twice a minute raises an alert, because ordinary traffic does not do that. Raising PortScan.MaxSources raises the attacker's cost proportionally.

  • A sweep of the network. One port across many hosts is, from this host, one packet. There is nothing to count.

  • Enumeration that never touches you: DNS, certificate transparency logs, search engines.

  • The exact number of ports, past the last sixty-four. Ports are deduplicated exactly against the last sixty-four a source was scored for, in fixed memory. A port seen again after it has left that window is counted again, so the figure is an upper bound; it is clamped to the observed port range, which is exact, so it can never claim more ports than the range could hold. For the contiguous ranges scanners actually walk the two coincide.

A source flooding from its own address can hide its own probes. Each source is held to 256 packets a second, and it is the first 256 of each second that are examined. Measured with a real nmap scan at 200 probes a second: at 2 000 packets a second from the same address the scan still crossed, at 20 000 only 3 of 200 probes got through and it did not. The turning point is around thirty times the probe rate, and small packets are enough. The refusals are counted (source_rate_limited) but not reported: a single busy peer, a download, does exactly the same, and warning on it would warn on every download. The flood itself is plainly visible to anything counting traffic.

Reconnaissance this sensor does not see

The nmap corpus answers one question (does the engine name the technique nmap was asked to use?) and it cannot answer another: what reconnaissance exists that nmap does not implement. nmap is one maintainer's tool on a slow release cycle, and several documented techniques live outside it, in NSE scripts, in other tools, or only in the literature.

testdata/recon/ holds ten of them, forged by hand with scapy from RFC 7707 (Network Reconnaissance in IPv6 Networks), from nmap's own targets-ipv6-multicast-* NSE scripts, and from the rate-limit side-channel literature. forge_recon.py builds them and cites the source of each. All ten are currently invisible to this sensor, and TestReconnaissanceBeyondNmap pins that measurement so the claim here cannot drift from the code.

Two structural reasons account for every one of them:

A probe aimed at a multicast or broadcast destination is discarded before scoring. Engine.accept requires one of this host's own unicast addresses. That is what keeps ordinary router advertisements, neighbour discovery, mDNS and SSDP from being scored, and it is also why the whole IPv6 link-local discovery family is invisible: ICMPv6 echo to ff02::1, the unrecognised-option probe that exists because Windows ignores multicast echo, MLD general queries, Router Solicitations, LLMNR and mDNS enumeration, and IPv4 broadcast ping. Since brute-force address scanning is impossible in IPv6, these are how hosts are found on a link.

A scan from a link-local address is a different matter and is fully counted. fe80::/10 and 169.254.0.0/16 are on the list that only forbids blocking, because the host needs its own link, not on the one that suppresses detection: an attacker on the link has such an address by default, the reported source MAC is the one piece of identifying evidence this feature can give, and suppressing those ranges would make that attacker invisible at no cost.

ICMP is scored by message TYPE, not by packet. It has no ports, so without this it would fall into the "other protocol" bucket and score nothing at all: a sweep of this host with ICMP queries would be worth zero, and so would the RFC 4620 Node Information Query that asks the host for its names.

Scoring it by packet is not the answer either. An echo request is the most ordinary packet on any network, and counting them would make a monitoring system's ping -f look like an attack: the worst thing this feature can do is cut the host off from something it needs. So the message type and code travel where the destination port would be, the engine deduplicates scored probes on the type alone, and a source is worth one score per distinct message type however many packets it sends, and whatever codes it puts in them.

  • Three distinct query types report an ICMP sweep. That is exactly what nmap -PE -PP -PM sends, and ICMPSweepMinTypes sets it.
  • Two of those three are not enough, by design. nmap -PP -PM without -PE sends timestamp and address-mask requests only: two distinct types, worth 40 points each against a threshold of 100, so nothing is reported. Lowering either the type count or the weight to catch it would reopen the false positives that restricting this detector to query types closed -- a single traceroute6 through one intermediate router produces three ICMP types of its own. The two probes still count towards the score, so a host sweep combined with any other probing crosses normally; it is the ICMP-only, two-type case that is deliberately left alone.
  • Only query types are counted: echo request, timestamp request, information request, address-mask request, and over IPv6 echo request and the Node Information Query. Replies and errors are not. Inbound ICMP is dominated by messages this host asked for, and an intermediate router (one that is not the default gateway, so not exempt) sends Time Exceeded, Destination Unreachable and Echo Reply during a single traceroute. Over IPv6, where path MTU discovery is mandatory, Packet Too Big, Time Exceeded and Echo Reply arrive from one router during a single traceroute6. Counting those would name a router as a scanner after three packets.
  • Unassigned and reserved types are not counted either. They are not evidence of anything, and the source address of a connectionless probe costs nothing to forge.
  • DetectICMPSweep turns this detector off on its own, leaving TCP, UDP and protocol-scan detection running.
  • Timestamp, information and address-mask requests, and the Node Information Query, also score on their own: they are dead protocols that nothing but a scanner has sent in decades. One is worth 40 of the 100 needed, so a single stray probe from a misconfigured device still reports nothing.
  • Echo request never scores by itself.

Measured: twelve packets across three types are reported; five hundred echo requests from one source are not. The ICMPv6 rate-limit side channel of arXiv 2210.13088 uses a single type, so it stays below the line: the deliberate cost of never reporting your own monitoring.

The multicast gap is not closed, and closing it means finding a rule narrow enough not to score every chatty device on the LAN. That is a design decision with a false-positive cost, not a bug fix. Until someone makes it, the honest statement is the one above: those techniques are not detected, and the tests say so.

One further case is worth separating out. rst-ratelimit-probe.pcap is sixty SYNs to a single port, measuring the global RST budget in order to use this host as an idle-scan reflector against a third party. Port deduplication reduces it to one counted probe, and that is correct: it is not a port scan of this host. But it is this host being made into an instrument, which no part of this feature currently reports.

Several daemons, one GUI

When one GUI watches several daemons, the list of blocked sources tells them apart only as far as the GUI itself does. The GUI identifies a node by its address without the port: two daemons behind the same IPv4 address, or two clients of the same unix socket, are one node. IPv6 is worse, because of an upstream defect in how the GUI reads a peer's address: it keeps only what comes before the first colon, so every IPv6 daemon whose address starts with the same group (2001:, for instance) is one node. Within a merged node, a daemon can answer in another's name: its list can stand for the other's, and a release meant for one can reach the other. This is upstream behaviour, shared by everything else the GUI does per node, and the IPv6 part is being fixed separately. With one daemon per address, and IPv4 or a unix socket, each node's list is its own.

Clone this wiki locally