iptables REJECT vs DROP

REJECT and DROP both discard the packet. The difference: DROP stays silent and costs the client a two-minute timeout, while REJECT answers in under a millisecond. With measurements, the —reject-with variants, and whether DROP really hides anything.

REJECT and DROP are both terminating targets. Either way the packet does not get through. What differs is what the sender learns afterwards: DROP discards it in silence, REJECT sends an error back.

That sounds like a matter of taste. It is measurable. In the test run below, the same connection attempt burns 133.5 seconds against DROP and fails in under a millisecond against REJECT. And the usual argument for DROP – that it makes the server invisible – only holds up halfway.

Say port 80 should be closed to incoming TCP connections:

iptables -A INPUT -p tcp --dport 80 -j DROP

Incoming TCP connections to port 80 are discarded and nothing goes back. The client is not told that its request was refused. It is told nothing at all, and it waits.

REJECT answers instead:

iptables -A INPUT -p tcp --dport 80 -j REJECT

The sender gets an error and can give up immediately. So far the textbook version. The interesting question is which error it gets.

What the client actually receives

With no further option, REJECT sends an ICMP destination unreachable packet with code port unreachable. The manual page puts it in five words: “icmp-port-unreachable is the default” (iptables-extensions(8)). For IPv6 the default is icmp6-port-unreachable.

What reaches the sender’s program is an error number, and this is where it gets surprising. The default produces ECONNREFUSED – the very same number a port with no listening service produces.

Reproduced on Linux 6.18 with iptables 1.8.10 (nf_tables). Each row is one connect() to port 8080, where a service is in fact running behind the rule:

connect() against a blocked port, measured on Linux 6.18 with iptables 1.8.10
RuleError numberMessageTime
no rule–connection established0.05 s
-j DROP110 ETIMEDOUTConnection timed out133.5 s
-j REJECT (default)111 ECONNREFUSEDConnection refusedunder 0.001 s
--reject-with tcp-reset111 ECONNREFUSEDConnection refusedunder 0.001 s
--reject-with icmp-host-unreachable113 EHOSTUNREACHNo route to hostunder 0.001 s
--reject-with icmp-admin-prohibited113 EHOSTUNREACHNo route to hostunder 0.001 s

Three things stand out.

First. The REJECT default is indistinguishable from a closed port. ECONNREFUSED is exactly what a machine reports when nobody is listening.

Second. tcp-reset gives the same result as the default, only through a TCP RST instead of ICMP. That matters wherever ICMP is filtered somewhere along the path and the message would never arrive.

Third. Only the host-unreachable and prohibited variants reveal that filtering is happening at all. EHOSTUNREACH says: something is in the way.

The —reject-with variants

For IPv4, REJECT knows eight types: icmp-net-unreachable, icmp-host-unreachable, icmp-port-unreachable, icmp-proto-unreachable, icmp-net-prohibited, icmp-host-prohibited, icmp-admin-prohibited and tcp-reset. IPv6 has its own list: icmp6-no-route, icmp6-adm-prohibited, icmp6-addr-unreachable, icmp6-port-unreachable and tcp-reset as well.

tcp-reset carries a condition the manual states outright: it “can be used on rules which only match the TCP protocol”. Without -p tcp the kernel refuses the rule:

# without -p tcp: refused
$ iptables -I INPUT 1 -i lo -j REJECT --reject-with tcp-reset
iptables v1.8.10 (nf_tables):  RULE_INSERT failed (Invalid argument): rule in chain INPUT

# with -p tcp: accepted
$ iptables -I INPUT 1 -i lo -p tcp --dport 8080 -j REJECT --reject-with tcp-reset

A second caveat concerns old kernels: “Using icmp-admin-prohibited with kernels that do not support it will result in a plain DROP instead of REJECT.” The rule is accepted and still behaves differently from what it says. On current systems this is moot. On frozen appliance kernels it is not.

Why DROP costs the client two minutes

The 133.5 seconds are not a property of iptables but of the TCP stack on the other side. When a SYN goes unanswered, the kernel retransmits it. How often is set by net.ipv4.tcp_syn_retries. tcp(7) gives the default as 6 and works out that this means “retrying for up to approximately 127 seconds”. The measurement came out at 133.5 seconds, which is the same order of magnitude.

Who that timeout actually hits is the real question.

  • A port scanner brings its own short timeouts and works in parallel. Silence costs it seconds, not minutes.
  • A monitoring check, a cron job or a deployment script hangs for the full two minutes on a connection that will never happen – possibly running into its own timeout on the way.
  • Anyone who has just locked themselves out reads the server as dead rather than closed, and starts looking in the wrong place.

Hence the rule of thumb: DROP facing outward, REJECT facing inward. Otherwise the timeout hits precisely the wrong people.

The stealth myth

“DROP makes the server invisible” is the most common justification, and it does not carry far. Nmap distinguishes explicitly between closed and filtered. A closed port is “accessible (it receives and responds to Nmap probe packets), but there is no application listening on it”; a filtered one is a port where “packet filtering prevents its probes from reaching the port” (Nmap documentation).

That distinction inverts the argument. DROP produces filtered, which announces that somebody is filtering. REJECT with its default produces closed, which announces that there is simply nothing here. The same documentation notes that filters which drop silently are “far more common” than those that answer with ICMP. Silence is the expected case, not a hiding place.

What DROP genuinely buys lies elsewhere: it does not answer. Every answer goes to the source address in the packet, and that address can be forged – a host that emits an ICMP message for every discarded packet can be used as a reflector. The kernel caps this on its own: icmp(7) gives icmp_ratelimit a default minimum gap of 1000 milliseconds per target, and the default icmp_ratemask of 0x1818 includes destination unreachable. So the effect is bounded. It is not zero.

Which target when

The decision comes down to four cases.

  • Internal network, your own clients, development machines: REJECT. An immediate “Connection refused” saves exactly the two minutes from the table while you are debugging. Whether anything is listening in the first place is worth checking beforehand by showing the used ports with ss or netstat.
  • Public-facing interface, default rule at the end of the chain: DROP. Not for the camouflage, but so the server sends nothing to forged senders.
  • Service switched off, and meant to look switched off: REJECT --reject-with icmp-admin-prohibited. This is the honest variant: it tells the other side that a policy applies, rather than that a cable is missing.
  • ICMP gets swallowed on the way: REJECT --reject-with tcp-reset, together with -p tcp. An RST gets through where an ICMP message gets stuck.

Two things that trip people up

Order decides, not intent. Both targets terminate the chain. A rule sitting below a matching DROP or REJECT is never reached. iptables -L INPUT -n --line-numbers shows the real order; -I inserts at the top, -A appends at the bottom.

Rules do not survive a reboot by themselves. Whatever iptables -A writes into the kernel is gone after the next boot. Making it permanent has an article of its own on restoring iptables rules from a file after a reboot.

iptables or nftables?

One piece of context at the end, because it concerns the commands above. The netfilter project calls nftables the successor: “nftables replaces the popular {ip,ip6,arp,eb}tables.” It has been available “upstream since Linux kernel 3.13” (netfilter.org).

iptables is still there. On current systems the command simply writes into the nftables backend instead of the old one. The version output tells you which path is in use:

$ iptables -V
iptables v1.8.10 (nf_tables)

(legacy) means the old interface, (nf_tables) the new one. Debian 13 “trixie” ships iptables 1.8.11-2 (Debian 13 manual page). None of this changes REJECT and DROP: same names, same behaviour. Written directly in nftables the equivalents are drop and reject, with reject with tcp reset for the RST variant.

Conclusion

DROP stays silent, REJECT answers. That is the whole difference, and also why the choice is not arbitrary.

The timeout DROP triggers runs to about two minutes and punishes your own scripts harder than any attacker. The REJECT default cannot be told apart from a closed port. Anyone hoping for camouflage gets it from DROP only in the sense that Nmap reports filtered instead of closed – which is to say, it reports the firewall. And anyone who wants a refusal to read as a refusal should say so explicitly with --reject-with icmp-admin-prohibited.

REJECT inside, DROP outside. And in both cases, test the rule against the real port once you have written it.