Firewall packet tracer (iptables and nftables)
Paste iptables-save or nft list ruleset output and a packet, and step it through the netfilter hooks in the kernel's order — every chain, every rule tested with the reason it matched or didn't, jumps, returns, policies, DNAT and MASQUERADE — to the final verdict; with Docker's and kube-proxy's chains explained.
Added
How to use it
When a port forward doesn’t work or a container can’t be reached, the answer is in the
ruleset, but iptables-save on a Docker or Kubernetes host is hundreds of lines across several
tables, and nftables can add more tables on the same hooks. This page walks one packet through
all of it in the order the kernel does and stops where the kernel would.
How to use it
-
Paste the output of
sudo iptables-save(legacy or nf_tables backend; add-cto keep the counters,ip6tables-savefor IPv6, andiptables -Sworks too) and/orsudo nft list ruleset. Paste both if the host has both — the kernel runs every table on a hook. Or start from an example: a small router, the same router in native nftables, Docker 29 with its iptables and its nftables backends, and a kind node’s kube-proxy in iptables mode and in nftables mode. -
Describe the packet: incoming to this host, forwarded through it, or locally generated; the protocol, addresses and ports, the interfaces, and the conntrack state (
NEWfor the first packet of a connection,ESTABLISHEDfor the rest). Under This host, list the host’s own addresses (for INPUT vs FORWARD andaddrtype/fib … type localmatches) and a few routes (10.0.0.0/8 dev eth1,default dev eth0) so the out-interface is right, including after DNAT. -
Read the result: the verdict and the rule that decided it, the address translations, and the path — each hook with its base chains in priority order. Below it, every chain the packet entered lists every rule it was tested against: ✓ with the actions it took, or ✕ with the first condition that failed and why (
destination port 22 is not 80).
It follows the kernel’s rules: on each hook the base chains run by priority (raw −300,
conntrack −200, mangle −150, DNAT −100, filter 0, security 50, SNAT 100; nftables chains slot in
by their own priorities, and for equal priorities the chain registered last runs first);
accept ends only the current base chain, so a later chain on the same hook can still drop the
packet; drop, reject and queue are final; NAT chains see only a connection’s first packet, and
once one NAT chain has translated it the remaining NAT chains on that hook are skipped; a jump
returns, a goto doesn’t; a base chain’s policy applies when its rules run out. Routing happens
after PREROUTING (a DNAT can turn an incoming packet into a forwarded one) and again after
OUTPUT.
Load balancing is random per connection: kube-proxy’s -m statistic --mode random
--probability rules and nftables numgen random mod n maps are listed under Random choices,
where you can follow another endpoint; each KUBE-SVC chain shows each endpoint’s share.
Well-known chains get a note: DOCKER-USER (where your own rules for published ports go — the packet is already DNATed there), DOCKER-FORWARD, DOCKER-CT, DOCKER-BRIDGE, DOCKER and its raw direct-access drop; KUBE-SERVICES, KUBE-SVC-, KUBE-SEP-, KUBE-EXT-, KUBE-NODEPORTS, KUBE-MARK-MASQ and KUBE-POSTROUTING; kube-proxy’s and Docker’s nftables tables; and the chains of Calico, Cilium, Flannel, ufw, fail2ban and libvirt.
Related tools: the container simulator shows how Docker’s bridge, veths and NAT fit together; the subnet calculator for CIDRs; the packet capture viewer to see what actually crossed the wire; and server config for the services behind the ports. For Kubernetes NetworkPolicies — which network plugins turn into rules like these — use the NetworkPolicy checker.
How faithful it is
The traces were checked against the kernel. In a throwaway privileged Debian container I set
up a router between two network namespaces, loaded the example ruleset, sent one packet per case
and read back which rules the kernel counted (iptables-save -c), with both the nf_tables and
the legacy backend; the tracer matches exactly those rules in all 13 cases, and the NATed
addresses match what tcpdump saw on the far side. The native nftables example was traced with
nft monitor trace, and the page lists the same rules and policies in the same order. The
Docker examples come from docker:29-dind (iptables and --firewall-backend=nftables) and were
checked against their counters after a real connection to a published port; the kube-proxy
examples come from a kind cluster and were checked against nft monitor trace for a ClusterIP
connection from the node and a NodePort connection from outside.
What it doesn’t model: matches whose result depends on earlier traffic or state outside the
ruleset — rate limits (assumed under the limit), recent, connlimit, quota, ipsets (their
contents aren’t in iptables-save), socket, ct label, physdev, cgroup and BPF matches. Those
are marked "?" and treated as matching unless you choose otherwise. It doesn’t do policy routing
(ip rule), reverse-path filtering, bridge-family or netdev (ingress) chains, IPVS, or eBPF
programs such as Cilium’s, which don’t appear in iptables or nft output at all. iptables -L
output leaves out too much; use iptables-save.
Feedback is a gift. Something to improve, a feature to add, or a bug? Tell me.
Runs entirely in your browser; nothing you type leaves this page.