• Bearbeitet:
  • #iptables

iptables REJECT vs DROP

REJECT und DROP verwerfen beide das Paket. Der Unterschied: DROP schweigt und kostet den Client rund zwei Minuten Timeout, REJECT antwortet in unter einer Millisekunde. Mit Messwerten, den —reject-with-Varianten und der Frage, ob DROP wirklich tarnt.

REJECT und DROP sind beides Endziele einer iptables-Regel. Das Paket kommt in beiden Fällen nicht durch. Der Unterschied liegt darin, was der Absender danach erfährt: DROP verwirft stillschweigend, REJECT schickt eine Fehlermeldung zurück.

Das klingt nach Geschmacksfrage. Es ist messbar. Derselbe Verbindungsversuch läuft im Testlauf weiter unten mit DROP 133,5 Sekunden ins Leere und scheitert mit REJECT in unter einer Millisekunde. Die übliche Begründung für DROP – der Server werde dadurch unsichtbar – hält dabei nur zur Hälfte.

Angenommen, Port 80 soll für eingehende TCP-Verbindungen dicht sein:

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

Eingehende TCP-Verbindungen auf Port 80 werden verworfen, ohne dass etwas zurückgeht. Der Client erfährt nicht, dass seine Anfrage abgelehnt wurde. Er erfährt gar nichts und wartet.

REJECT antwortet stattdessen:

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

Der Absender bekommt eine Fehlermeldung und kann sofort aufgeben. So weit die Lehrbuchfassung. Interessant wird die Frage, welche Fehlermeldung das ist.

Was der Client tatsächlich zurückbekommt

Ohne weitere Angabe schickt REJECT ein ICMP-Paket vom Typ destination unreachable mit dem Code port unreachable. Die Handbuchseite der iptables-Erweiterungen sagt es in fünf Wörtern: „icmp-port-unreachable is the default“ (iptables-extensions(8)). Bei IPv6 ist die Voreinstellung icmp6-port-unreachable.

Im Programm des Absenders kommt davon eine Fehlernummer an. Und genau dort wird es unerwartet: Die Voreinstellung führt zu ECONNREFUSED – derselben Nummer, die ein Port ohne lauschenden Dienst erzeugt.

Nachgestellt auf Linux 6.18 mit iptables 1.8.10 (nf_tables). Jeweils ein connect() auf Port 8080, auf dem ein Dienst läuft, den die Regel davor abschneidet:

connect() auf einen abgeschnittenen Port, gemessen auf Linux 6.18 mit iptables 1.8.10
RegelFehlernummerMeldungDauer
keine Regel–Verbindung steht0,05 s
-j DROP110 ETIMEDOUTConnection timed out133,5 s
-j REJECT (Voreinstellung)111 ECONNREFUSEDConnection refusedunter 0,001 s
--reject-with tcp-reset111 ECONNREFUSEDConnection refusedunter 0,001 s
--reject-with icmp-host-unreachable113 EHOSTUNREACHNo route to hostunter 0,001 s
--reject-with icmp-admin-prohibited113 EHOSTUNREACHNo route to hostunter 0,001 s

Drei Dinge fallen auf.

Erstens. Die Voreinstellung von REJECT ist von einem geschlossenen Port nicht zu unterscheiden. ECONNREFUSED ist genau das, was ein Rechner meldet, auf dem niemand lauscht.

Zweitens. tcp-reset führt zum selben Ergebnis wie die Voreinstellung, nur über ein TCP-RST statt über ICMP. Das zählt überall dort, wo ICMP unterwegs gefiltert wird und die Meldung sonst nie ankäme.

Drittens. Erst die host-unreachable- und prohibited-Varianten verraten, dass überhaupt gefiltert wird. EHOSTUNREACH sagt: Hier steht etwas dazwischen.

Die Varianten von —reject-with

Für IPv4 kennt REJECT acht Typen: icmp-net-unreachable, icmp-host-unreachable, icmp-port-unreachable, icmp-proto-unreachable, icmp-net-prohibited, icmp-host-prohibited, icmp-admin-prohibited und tcp-reset. Für IPv6 gilt eine eigene Liste mit icmp6-no-route, icmp6-adm-prohibited, icmp6-addr-unreachable, icmp6-port-unreachable und ebenfalls tcp-reset.

An tcp-reset hängt eine Bedingung, die das Handbuch ausdrücklich nennt: Es „can be used on rules which only match the TCP protocol“. Ohne -p tcp nimmt der Kernel die Regel nicht an:

# ohne -p tcp: abgelehnt
$ 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

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

Eine zweite Einschränkung betrifft alte Kernel: „Using icmp-admin-prohibited with kernels that do not support it will result in a plain DROP instead of REJECT.“ Die Regel wird also angenommen und verhält sich trotzdem anders, als sie dasteht. Auf aktuellen Systemen spielt das keine Rolle. Auf eingefrorenen Appliance-Kerneln schon.

Warum DROP den Client zwei Minuten kostet

Die 133,5 Sekunden aus der Tabelle sind keine Eigenschaft von iptables, sondern eine des TCP-Stacks auf der Gegenseite. Bleibt ein SYN unbeantwortet, wiederholt der Kernel es. Wie oft, steht in net.ipv4.tcp_syn_retries. tcp(7) nennt als Voreinstellung 6 und rechnet vor, das entspreche „retrying for up to approximately 127 seconds“. Gemessen wurden 133,5 Sekunden; die Größenordnung stimmt.

Wen dieser Timeout trifft, ist die eigentliche Frage.

  • Ein Portscanner bringt eigene, kurze Zeitlimits mit und arbeitet parallel. Ihn kostet das Schweigen Sekunden, nicht Minuten.
  • Ein Monitoring-Check, ein Cronjob oder ein Deployment-Skript hängt dagegen die vollen zwei Minuten an einer Verbindung, die nie zustande kommt – und läuft dabei möglicherweise in sein eigenes Zeitlimit.
  • Wer sich gerade selbst ausgesperrt hat, hält den Server für tot statt für dicht und sucht an der falschen Stelle.

Daher die Faustregel: DROP nach außen, REJECT nach innen. Sonst trifft der Timeout genau die Falschen.

Der Tarnungs-Mythos

„DROP macht den Server unsichtbar“ ist die meistgenannte Begründung – und sie trägt nicht weit. Nmap unterscheidet ausdrücklich zwischen closed und filtered. Ein geschlossener Port ist „accessible (it receives and responds to Nmap probe packets), but there is no application listening on it“, ein gefilterter einer, bei dem „packet filtering prevents its probes from reaching the port“ (Nmap-Dokumentation).

Diese Unterscheidung dreht das Argument um. DROP erzeugt filtered und sagt damit: Hier filtert jemand. REJECT mit der Voreinstellung erzeugt closed und sagt: Hier ist schlicht nichts. Dieselbe Dokumentation hält fest, dass stillschweigend verwerfende Filter „far more common“ sind als solche mit ICMP-Antwort. Schweigen ist also das Erwartbare, kein Versteck.

Was DROP wirklich bringt, liegt woanders: Es antwortet nicht. Jede Antwort geht an die Absenderadresse im Paket, und die kann gefälscht sein – ein Host, der auf jedes verworfene Paket eine ICMP-Meldung schickt, lässt sich als Reflektor benutzen. Der Kernel begrenzt das von sich aus: icmp(7) nennt für icmp_ratelimit einen Mindestabstand von 1000 Millisekunden je Ziel, und die Voreinstellung von icmp_ratemask (0x1818) schließt destination unreachable ein. Die Wirkung ist damit gedeckelt, aber nicht null.

Wann welches Ziel

Die Entscheidung lässt sich auf vier Fälle eindampfen.

  • Internes Netz, eigene Clients, Entwicklungsmaschinen: REJECT. Ein sofortiges „Connection refused“ spart bei der Fehlersuche genau die zwei Minuten aus der Tabelle. Ob überhaupt jemand lauscht, zeigt vorher ein Blick auf die genutzten Ports mit ss oder netstat.
  • Öffentliche Schnittstelle, Standardregel am Kettenende: DROP. Nicht wegen der Tarnung, sondern weil der Server nichts an gefälschte Absender schicken soll.
  • Dienst abgeschaltet, soll aber als abgeschaltet erkennbar sein: REJECT --reject-with icmp-admin-prohibited. Das ist die ehrliche Variante. Sie sagt dem Gegenüber, dass hier eine Richtlinie greift und nicht ein Kabel fehlt.
  • ICMP wird unterwegs geschluckt: REJECT --reject-with tcp-reset, zusammen mit -p tcp. Ein RST kommt durch, wo eine ICMP-Meldung hängen bleibt.

Zwei Stolperstellen

Die Reihenfolge entscheidet, nicht die Absicht. Beide Ziele beenden die Kette. Eine Regel unterhalb einer passenden DROP- oder REJECT-Regel wird nie erreicht. iptables -L INPUT -n --line-numbers zeigt die tatsächliche Reihenfolge; -I schiebt an den Anfang, -A hängt ans Ende.

Regeln überleben den Neustart nicht von allein. Was mit iptables -A in den Kernel geschrieben wird, ist nach dem nächsten Boot weg. Wie man sie dauerhaft macht, steht in einem eigenen Artikel darüber, wie sich iptables nach einem Neustart wiederherstellen lässt.

iptables oder nftables?

Eine Einordnung zum Schluss, weil sie die Befehle oben betrifft. Das Netfilter-Projekt bezeichnet nftables als Nachfolger: „nftables replaces the popular {ip,ip6,arp,eb}tables.“ Verfügbar ist es „upstream since Linux kernel 3.13“ (netfilter.org).

iptables gibt es trotzdem weiter. Auf aktuellen Systemen schreibt der Befehl nur in den nftables-Unterbau statt in den alten. Welcher Weg läuft, verrät die Versionsausgabe:

$ iptables -V
iptables v1.8.10 (nf_tables)

Steht dort (legacy), arbeitet die alte Schnittstelle, bei (nf_tables) die neue. Debian 13 „trixie“ liefert iptables in Version 1.8.11-2 aus (Handbuchseite von Debian 13). An REJECT und DROP ändert das nichts: Beide heißen gleich und verhalten sich gleich. Wer die Regeln direkt in nftables schreibt, nutzt drop beziehungsweise reject, und für die RST-Variante reject with tcp reset.

Fazit

DROP schweigt, REJECT antwortet. Das ist der ganze Unterschied und zugleich der Grund, warum die Wahl nicht beliebig ist.

Der Timeout, den DROP auslöst, liegt bei rund zwei Minuten und trifft eigene Skripte härter als jeden Angreifer. Die Voreinstellung von REJECT ist von einem geschlossenen Port nicht zu unterscheiden. Wer Tarnung sucht, bekommt sie durch DROP nur in der Form, dass Nmap statt closed eben filtered meldet – und damit ausgerechnet die Existenz der Firewall meldet. Wer will, dass die Ablehnung als Ablehnung erkennbar ist, sagt das mit --reject-with icmp-admin-prohibited ausdrücklich.

Innen REJECT, außen DROP. Und in beiden Fällen die Regel nach dem Schreiben einmal gegen den echten Port testen.