Every Linux box you have ever logged into was already running a packet filter. It had no rules in it, so it did nothing, but the machinery was there: five tables, a fixed set of chains hooked into defined points in the kernel's packet path, and a userspace tool called iptables that writes rules into them. Learning that tool is less about memorizing flags and more about knowing which chain a packet passes through and in what order, because a rule in the wrong chain is a rule that never fires.
This article is part of the Linux networking commands guide. Everything below was captured on a Debian 13 host bridged into a CML topology. The host sits at 10.77.0.100 on ens224, an nginx server (WEB1) is two router hops away at 10.77.2.10, and a server running iperf3 on 5201 and a Python HTTP server on 8080 (SRV1) is three hops away at 10.77.3.10. Every rule below is scoped to that lab interface, because the management NIC is how the session stays alive.
The first thing to know: iptables is not iptables anymore
On Debian 13, and on every current mainstream distribution, the binary called iptables is a compatibility shim over nftables. It reports this itself if you look:
j@llmbits:~$ sudo iptables --version
iptables v1.8.11 (nf_tables)
j@llmbits:~$ ls -l /usr/sbin/iptables
lrwxrwxrwx 1 root root 26 Nov 20 2024 /usr/sbin/iptables -> /etc/alternatives/iptables
j@llmbits:~$ sudo update-alternatives --display iptables | head -5
iptables - auto mode
link best version is /usr/sbin/iptables-nft
link currently points to /usr/sbin/iptables-nft
link iptables is /usr/sbin/iptables
slave iptables-restore is /usr/sbin/iptables-restore(nf_tables) in the version string is the part that matters. The command syntax you are about to learn is unchanged, and rules you write with it behave the way they always did, but they are stored in the nftables engine rather than the old x_tables one. That has one practical consequence: rules written with iptables are visible to nft, and vice versa, so mixing the two tools on one host produces a ruleset that neither tool fully explains. More on that in the nftables article.
Tables and chains
A table is a group of chains that share a purpose. A chain is an ordered list of rules attached to a point in the kernel's packet path. You only ever need three of the five tables in practice.
NOTRACK, exempting high-volume traffic from conntrack to save table space.Which chain a packet hits depends entirely on where it is going, and this is the single most common source of "my rule does nothing":
curl, ping and DNS queries hit OUTPUT.A default Debian install has all three chains present, all policies set to ACCEPT, and no rules at all:
j@llmbits:~$ sudo iptables -L -n -v
Chain INPUT (policy ACCEPT 907K packets, 80M bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination-L lists, -n skips DNS and service-name resolution (always use it, otherwise listing a busy ruleset stalls on reverse lookups), and -v adds the interface columns and the per-rule packet and byte counters. Those counters are the most useful diagnostic iptables gives you, and half this article is really about reading them.
The other listing format is -S, which prints rules as the commands that would recreate them:
j@llmbits:~$ sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPTUse -L -n -v when you want counters. Use -S when you want something you can copy, paste or diff.
The first rule, and what DROP and REJECT really do
Before touching anything, here is the baseline. SRV1 has two ports open:
j@llmbits:~$ sudo nmap -Pn -p 22,80,5201,8080 10.77.3.10
Starting Nmap 7.95 ( https://nmap.org ) at 2026-08-19 10:07 PDT
Nmap scan report for 10.77.3.10
Host is up (0.0058s latency).
PORT STATE SERVICE
22/tcp closed ssh
80/tcp closed http
5201/tcp open targus-getdata1
8080/tcp open http-proxy
j@llmbits:~$ nc -zv -w 3 10.77.3.10 5201
Connection to 10.77.3.10 5201 port [tcp/*] succeeded!Now one rule. Note the scoping: -o ens224 pins it to the lab NIC and -d 10.77.3.10 pins it to one destination, so nothing on the management path can possibly be caught by it.
j@llmbits:~$ sudo iptables -A OUTPUT -o ens224 -d 10.77.3.10 -p tcp --dport 5201 -j REJECT
j@llmbits:~$ sudo iptables -S OUTPUT
-P OUTPUT ACCEPT
-A OUTPUT -d 10.77.3.10/32 -o ens224 -p tcp -m tcp --dport 5201 -j REJECT --reject-with icmp-port-unreachableTwo things happened that you did not type. iptables expanded -p tcp --dport into an explicit -m tcp match module load, and it filled in the default rejection method, icmp-port-unreachable. Always read a rule back with -S after writing it; the expanded form is what the kernel is actually running.
Now the behavior, with timing, because timing is the whole difference:
j@llmbits:~$ time nc -zv -w 5 10.77.3.10 5201
nc: connect to 10.77.3.10 port 5201 (tcp) failed: Connection refused
real 0m0.006sSix milliseconds. The kernel generated an ICMP port unreachable back to the local socket immediately, and nc reported a refusal. Swap the target for DROP and run the identical test:
j@llmbits:~$ sudo iptables -R OUTPUT 1 -o ens224 -d 10.77.3.10 -p tcp --dport 5201 -j DROP
j@llmbits:~$ time nc -zv -w 5 10.77.3.10 5201
nc: connect to 10.77.3.10 port 5201 (tcp) timed out: Operation now in progress
real 0m5.011sFive seconds, which is exactly the -w 5 timeout. The SYN went into a black hole, the client retransmitted, and nothing ever came back. Scanners see the same distinction:
j@llmbits:~$ sudo nmap -Pn -p 5201,8080 10.77.3.10
PORT STATE SERVICE
5201/tcp filtered targus-getdata1
8080/tcp open http-proxy
Offending packet: TCP 10.77.0.100:48786 > 10.77.3.10:5201 S ttl=59 id=22171 iplen=44 seq=2324308949 win=1024 <mss 1460>
sendto in send_ip_packet_sd: sendto(3, packet, 44, 0, 10.77.3.10, 16) => Operation not permittedfiltered, not closed. That word is nmap telling you a firewall swallowed the probe rather than a host refusing it, and the Operation not permitted error underneath is nmap's raw socket being blocked by our own OUTPUT rule. The -R in that command, incidentally, is replace: -R OUTPUT 1 overwrites rule 1 in place rather than appending a second rule that would never be reached.
There is a third option, a TCP reset instead of an ICMP error:
j@llmbits:~$ sudo iptables -R OUTPUT 1 -o ens224 -d 10.77.3.10 -p tcp --dport 5201 -j REJECT --reject-with tcp-reset
j@llmbits:~$ time nc -zv -w 5 10.77.3.10 5201
nc: connect to 10.77.3.10 port 5201 (tcp) failed: Connection refused
real 0m0.006s
filtered, because nmap treats an ICMP type 3 unreachable as evidence of a firewall.closed rather than filtered.One caveat about that scanner column, because the lab makes it easy to draw the wrong conclusion. The rule above is on the OUTPUT chain of the scanning host itself, so nmap reports filtered for both reject methods and prints Operation not permitted: its raw socket send is being blocked and no probe ever leaves the box. The closed against filtered distinction only shows up when the rule is on the target's INPUT chain and the scanner is somewhere else. Verified both ways in this lab, and it is a useful reminder that a scan result describes the whole path, not just the destination.
The operational rule of thumb: DROP on the perimeter, REJECT internally. A DROP inside your own network turns a five-line config error into a thirty-second application timeout that someone will page you about.
Counters are the debugger
After a single connection attempt:
j@llmbits:~$ sudo iptables -L OUTPUT -n -v
Chain OUTPUT (policy ACCEPT 19 packets, 2644 bytes)
pkts bytes target prot opt in out source destination
1 60 REJECT tcp -- * ens224 0.0.0.0/0 10.77.3.10 tcp dpt:5201 reject-with icmp-port-unreachableOne packet, sixty bytes, a single SYN. When somebody tells you a firewall rule is not working, this is the first thing to look at, and the answer is almost always one of two things. Either the counter is zero, meaning the traffic is not matching this rule at all (wrong chain, wrong interface, wrong direction), or the counter is incrementing and the traffic is being blocked exactly as configured and the argument is about intent, not mechanism. Counters turn firewall debugging from an argument into an observation.
Rules are evaluated top to bottom, first match wins
This is the same first-match logic as a Cisco ACL, and it catches people the same way. Append a broad block, then append a specific allow:
j@llmbits:~$ sudo iptables -A OUTPUT -o ens224 -d 10.77.3.10 -j DROP
j@llmbits:~$ sudo iptables -A OUTPUT -o ens224 -d 10.77.3.10 -p tcp --dport 8080 -j ACCEPT
j@llmbits:~$ sudo iptables -L OUTPUT -n -v --line-numbers
Chain OUTPUT (policy ACCEPT 220 packets, 31619 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DROP all -- * ens224 0.0.0.0/0 10.77.3.10
2 0 0 ACCEPT tcp -- * ens224 0.0.0.0/0 10.77.3.10 tcp dpt:8080
j@llmbits:~$ nc -zv -w 3 10.77.3.10 8080
nc: connect to 10.77.3.10 port 8080 (tcp) timed out: Operation now in progressThe allow is unreachable. Rule 1 matches everything to that host and the packet never reaches rule 2. Delete the allow and insert it at the top instead, which is what -I does:
j@llmbits:~$ sudo iptables -D OUTPUT 2
j@llmbits:~$ sudo iptables -I OUTPUT 1 -o ens224 -d 10.77.3.10 -p tcp --dport 8080 -j ACCEPT
j@llmbits:~$ sudo iptables -L OUTPUT -n -v --line-numbers
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT tcp -- * ens224 0.0.0.0/0 10.77.3.10 tcp dpt:8080
2 3 180 DROP all -- * ens224 0.0.0.0/0 10.77.3.10
j@llmbits:~$ nc -zv -w 3 10.77.3.10 8080
Connection to 10.77.3.10 8080 port [tcp/http-alt] succeeded!
j@llmbits:~$ nc -zv -w 3 10.77.3.10 5201
nc: connect to 10.77.3.10 port 5201 (tcp) timed out: Operation now in progressSame two rules, opposite order, opposite result. Remember the shape of the four editing flags: -A appends to the end, -I inserts (at position 1 by default), -R replaces a numbered rule, and -D deletes either by number or by repeating the full rule specification. --line-numbers is what makes the numbered forms usable.
Direction is not the same as reachability
A rule that drops replies looks exactly like a dead host from the application's point of view, and nothing like one on the wire. Watch:
j@llmbits:~$ sudo iptables -A INPUT -i ens224 -s 10.77.3.10 -p icmp --icmp-type echo-reply -j DROP
j@llmbits:~$ ping -c 3 -W 2 10.77.3.10
PING 10.77.3.10 (10.77.3.10) 56(84) bytes of data.
--- 10.77.3.10 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2051ms100% loss. Now capture on the same interface while pinging:
j@llmbits:~$ sudo tcpdump -ni ens224 -c 4 icmp
10:09:40.659850 IP 10.77.0.100 > 10.77.3.10: ICMP echo request, id 38, seq 1, length 64
10:09:40.665234 IP 10.77.3.10 > 10.77.0.100: ICMP echo reply, id 38, seq 1, length 64
10:09:41.663150 IP 10.77.0.100 > 10.77.3.10: ICMP echo request, id 38, seq 2, length 64
10:09:41.668447 IP 10.77.3.10 > 10.77.0.100: ICMP echo reply, id 38, seq 2, length 64The replies are arriving. tcpdump sits below netfilter on the receive path, so it sees packets the firewall is about to discard. That gap between "tcpdump sees it" and "the application does not" is the signature of a local INPUT rule, and it is worth internalizing, because it saves you from blaming the network for something happening in your own kernel. The counter confirms it:
j@llmbits:~$ sudo iptables -L INPUT -n -v
Chain INPUT (policy ACCEPT 909K packets, 80M bytes)
pkts bytes target prot opt in out source destination
5 420 DROP icmp -- ens224 * 10.77.3.10 0.0.0.0/0 icmptype 0If you want more on reading captures at this layer, the tcpdump reference and tshark in the terminal cover it properly.
A real policy: user chains, conntrack and logging
Everything so far has been single rules. A policy that you would actually deploy has three more ingredients: a user-defined chain to keep the scope contained, a connection tracking rule so you match flows instead of packets, and logging so you can see what got denied.
j@llmbits:~$ sudo iptables -N LAB-OUT
j@llmbits:~$ sudo iptables -A OUTPUT -o ens224 -d 10.77.0.0/16 -j LAB-OUT
j@llmbits:~$ sudo iptables -A LAB-OUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
j@llmbits:~$ sudo iptables -A LAB-OUT -p tcp --dport 8080 -j ACCEPT
j@llmbits:~$ sudo iptables -A LAB-OUT -j LOG --log-prefix 'LABFW-DROP ' --log-level 4
j@llmbits:~$ sudo iptables -A LAB-OUT -j DROP
j@llmbits:~$ sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N LAB-OUT
-A OUTPUT -d 10.77.0.0/16 -o ens224 -j LAB-OUT
-A LAB-OUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A LAB-OUT -p tcp -m tcp --dport 8080 -j ACCEPT
-A LAB-OUT -j LOG --log-prefix "LABFW-DROP "
-A LAB-OUT -j DROPThe pattern is worth stealing. One rule in the built-in chain decides what is in scope (traffic leaving ens224 for the lab range) and jumps to a named chain; everything else in that named chain is unconditional, so you never have to repeat the interface and subnet match on every line. It reads better, and it means the built-in chain stays short enough to audit.
Testing it:
j@llmbits:~$ curl -sS -o /dev/null -w 'HTTP %{http_code} in %{time_total}s\n' http://10.77.3.10:8080/
HTTP 200 in 0.014590s
j@llmbits:~$ nc -zv -w 3 10.77.3.10 5201
nc: connect to 10.77.3.10 port 5201 (tcp) timed out: Operation now in progress
j@llmbits:~$ ping -c 2 -W 2 10.77.3.10
2 packets transmitted, 0 received, 100% packet loss, time 1005msAnd the counters show where each packet landed:
j@llmbits:~$ sudo iptables -L LAB-OUT -n -v
Chain LAB-OUT (1 references)
pkts bytes target prot opt in out source destination
5 339 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
1 60 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080
5 348 LOG all -- * * 0.0.0.0/0 0.0.0.0/0 LOG flags 0 level 4 prefix "LABFW-DROP "
5 348 DROP all -- * * 0.0.0.0/0 0.0.0.0/0Read that carefully. Exactly one packet matched the port 8080 rule, the SYN. The other five packets of that HTTP exchange matched the conntrack rule instead, because once the flow is established the kernel recognizes it without re-evaluating the port match. That is what stateful means in practice, and it is why the ESTABLISHED rule goes first: it is the cheapest match in the chain and it handles the overwhelming majority of packets.
LOG is not a terminating target. The packet continues to the next rule, which is why the LOG and DROP counters are identical. The output goes to the kernel ring buffer:
j@llmbits:~$ sudo dmesg | grep LABFW-DROP | tail -4
[239999.964071] LABFW-DROP IN= OUT=ens224 SRC=10.77.0.100 DST=10.77.3.10 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=18170 DF PROTO=TCP SPT=57928 DPT=5201 WINDOW=64240 RES=0x00 SYN URGP=0
[240001.966793] LABFW-DROP IN= OUT=ens224 SRC=10.77.0.100 DST=10.77.3.10 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=16269 DF PROTO=ICMP TYPE=8 CODE=0 ID=40 SEQ=1Empty IN= and populated OUT= is the signature of a locally generated packet. Give every LOG rule a distinct prefix, because on a busy box the kernel log is shared with everything else and grepping for the prefix is the only sane way to find your own entries. And put a rate limit on any LOG rule you leave in production, or a scan will fill the disk for you.
Get the Linux Networking Field Reference - 10 pages, free
Every command in this cluster on ten printable pages: iproute2, sockets, DNS, capture, monitors and all three firewall front ends. Includes a ten-symptom troubleshooting decision tree and annotated real lab output. Free for PingLabz members, just sign up with your email.
Matching more than one thing at a time
Two match extensions save a lot of typing. multiport collapses a list of ports into one rule:
j@llmbits:~$ sudo iptables -A OUTPUT -o ens224 -d 10.77.3.10 -p tcp -m multiport --dports 22,80,5201,8080 -j REJECT
j@llmbits:~$ sudo iptables -S OUTPUT
-A OUTPUT -d 10.77.3.10/32 -o ens224 -p tcp -m multiport --dports 22,80,5201,8080 -j REJECT --reject-with icmp-port-unreachable
j@llmbits:~$ nc -zv -w 2 10.77.3.10 5201
nc: connect to 10.77.3.10 port 5201 (tcp) failed: Connection refused
j@llmbits:~$ nc -zv -w 2 10.77.3.10 8080
nc: connect to 10.77.3.10 port 8080 (tcp) failed: Connection refusedAnd limit applies a token bucket, which is how you rate limit ICMP or new connections without dropping everything:
j@llmbits:~$ sudo iptables -A OUTPUT -o ens224 -d 10.77.0.0/16 -p icmp --icmp-type echo-request -m limit --limit 2/sec --limit-burst 2 -j ACCEPT
j@llmbits:~$ sudo iptables -A OUTPUT -o ens224 -d 10.77.0.0/16 -p icmp --icmp-type echo-request -j DROP
j@llmbits:~$ sudo ping -c 20 -i 0.1 -W 1 10.77.3.10 | tail -4
20 packets transmitted, 5 received, 75% packet loss, time 1955ms
rtt min/avg/max/mdev = 5.191/5.478/5.863/0.237 ms
j@llmbits:~$ sudo iptables -L OUTPUT -n -v
pkts bytes target prot opt in out source destination
5 420 ACCEPT icmp -- * ens224 0.0.0.0/0 10.77.0.0/16 icmptype 8 limit: avg 2/sec burst 2
15 1260 DROP icmp -- * ens224 0.0.0.0/0 10.77.0.0/16 icmptype 8Twenty pings sent at ten per second, five allowed, fifteen dropped. Note the shape of it: limit matches only while there are tokens in the bucket, so the ACCEPT stops matching and the following DROP catches the rest. A limit rule always needs a partner rule beneath it.
The nat table, briefly
NAT deserves its own article, but one demonstration makes the mechanism clear. A DNAT rule in the OUTPUT chain rewrites the destination of locally generated packets before routing:
j@llmbits:~$ sudo iptables -t nat -A OUTPUT -o ens224 -d 10.77.3.10 -p tcp --dport 5201 -j DNAT --to-destination 10.77.3.10:8080
j@llmbits:~$ sudo iptables -t nat -L OUTPUT -n -v
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 DNAT tcp -- * ens224 0.0.0.0/0 10.77.3.10 tcp dpt:5201 to:10.77.3.10:8080
j@llmbits:~$ curl -sS -m 5 -o /dev/null -w 'HTTP %{http_code} answered on port 5201\n' http://10.77.3.10:5201/
HTTP 200 answered on port 5201curl asked for port 5201 and got an HTTP 200, because the kernel rewrote the destination port on the way out and rewrote the replies back on the way in. The two rules that matter in real deployments are the same idea at different hook points: -t nat -A PREROUTING ... -j DNAT to publish an internal service, and -t nat -A POSTROUTING -o <wan> -j MASQUERADE to give a subnet outbound access through a single address. Remember that only the first packet of a connection is evaluated in the nat table; conntrack applies the same translation to the rest of the flow, which is why the byte counters on a NAT rule are always far lower than the traffic passing through it. Order matters here too: for a locally generated packet the nat OUTPUT chain runs before filter OUTPUT, so a filter rule does see the rewritten destination, but POSTROUTING runs after filter, so you can never match on a post-SNAT source address in the filter table.
The same policy on a Cisco router
If you already write ACLs, the mapping is close enough to be useful. Here is the same "block one TCP port to one host" policy on R3, the IOS XE router directly in front of SRV1:
R3(config)#ip access-list extended LAB-FILTER
R3(config-ext-nacl)# deny tcp any host 10.77.3.10 eq 5201
R3(config-ext-nacl)# permit ip any any
R3(config-ext-nacl)#exit
R3(config)#interface Ethernet0/1
R3(config-if)# ip access-group LAB-FILTER outj@llmbits:~$ nc -zv -w 4 10.77.3.10 5201
nc: connect to 10.77.3.10 port 5201 (tcp) failed: No route to host
j@llmbits:~$ nc -zv -w 4 10.77.3.10 8080
Connection to 10.77.3.10 8080 port [tcp/http-alt] succeeded!R3#show ip access-lists LAB-FILTER
Extended IP access list LAB-FILTER
10 deny tcp any host 10.77.3.10 eq 5201 (1 match)
20 permit ip any any (16 matches)show ip access-lists is iptables -L -n -v: same job, same value, hit counters per line. The differences are real, though. The Cisco ACL ends in an implicit deny and iptables chains end in a policy you set explicitly. The Cisco ACL is applied to an interface in a direction, while iptables selects the interface with -i or -o inside the rule. And a classic IOS ACL is stateless unless you add established or reflexive entries, where -m conntrack is the default way to write Linux rules. Note also the client-side error. The local REJECT earlier produced "Connection refused" and the router's ACL produced "No route to host" for the same policy, because IOS answers an ACL denial with ICMP type 3 code 13, administratively prohibited, which Linux maps to EHOSTUNREACH rather than the ECONNREFUSED that a port unreachable produces. Two different messages, one blocked port, and the wording tells you which device did it.
Saving, restoring and the thing that will bite you
iptables-save dumps the live ruleset in a format iptables-restore can reload atomically:
j@llmbits:~$ sudo iptables-save -t filter
# Generated by iptables-save v1.8.11 (nf_tables) on Wed Aug 19 10:10:50 2026
*filter
:INPUT ACCEPT [908895:79769865]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [334:50632]
:LAB-OUT - [0:0]
-A OUTPUT -d 10.77.0.0/16 -o ens224 -j LAB-OUT
-A LAB-OUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A LAB-OUT -p tcp -m tcp --dport 8080 -j ACCEPT
-A LAB-OUT -j LOG --log-prefix "LABFW-DROP "
-A LAB-OUT -j DROP
COMMIT
# Completed on Wed Aug 19 10:10:50 2026Nothing in that ruleset survives a reboot on its own. iptables rules live in kernel memory and nowhere else. Persistence is a separate package (iptables-persistent on Debian and Ubuntu, which reloads from /etc/iptables/rules.v4), or a systemd unit, or your configuration management. Every engineer gets caught by this exactly once.
The counterpart trap is losing the box. If you set -P INPUT DROP before adding an allow rule for your own SSH session, the session dies with the command. Two habits prevent it: add the allow rules first and set the policy last, and on anything remote, arrange a rollback in advance. A background job is enough:
j@llmbits:~$ setsid nohup bash -c 'sleep 300; iptables -F; iptables -P INPUT ACCEPT' &If your new policy works you cancel it; if it does not, the box lets you back in five minutes later. That single line has saved more remote sessions than any amount of care.
Teardown is -F to flush rules and -X to delete empty user chains:
j@llmbits:~$ sudo iptables -F
j@llmbits:~$ sudo iptables -X LAB-OUT
j@llmbits:~$ sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
j@llmbits:~$ nc -zv -w 3 10.77.3.10 5201
Connection to 10.77.3.10 5201 port [tcp/*] succeeded!
j@llmbits:~$ ping -c 2 10.77.3.10 | tail -3
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 4.155/4.979/5.803/0.824 ms-F flushes only the table you are looking at. Flushing filter leaves nat and mangle untouched, and flushing IPv4 leaves IPv6 completely alone. A host is not clean until iptables -S, iptables -t nat -S and ip6tables -S are all empty.
Key takeaways
- The chain is chosen by the packet's destiny: INPUT for traffic to this host, OUTPUT for traffic from it, FORWARD for traffic through it. A rule in the wrong chain never fires.
iptables --versionreporting(nf_tables)means you are driving the nftables engine through a compatibility layer. Rules are visible to both tools.- DROP costs the client its full connect timeout, measured here at 5.011s against 0.006s for REJECT. Only
--reject-with tcp-resetreads asclosedto a remote scanner; an ICMP reject still reads asfiltered. DROP outward, REJECT inward. - Per-rule packet counters from
iptables -L -n -vsettle almost every "the firewall is broken" argument in one command. - First match wins, so
-Iand-Aare not interchangeable. Use--line-numbersand read the chain back before you trust it. - If tcpdump sees the packet and the application does not, the packet died in an INPUT rule on this host.
- Put
-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTfirst. In the capture above it matched five of the six packets in a single HTTP request. LOGdoes not terminate; the packet falls through to the next rule. Always give it a unique prefix and a rate limit.- Nothing persists across a reboot without
iptables-persistentor equivalent, and-Fonly clears the one table and one address family you named.
iptables is the syntax the whole industry still writes and reads, and it is not going away, but the engine underneath it moved on. nftables is what that engine looks like when you address it directly, and ufw is the two-command front end for the days when you just need port 22 open and nothing else. All three, along with the rest of the toolkit, are indexed in the complete guide to Linux networking commands.