Attack surface · iptables
iptables rule not working: the DROP is listed but traffic still passes (Debian 13, iptables-nft)
Published · HardStacked · 12 min
You added an iptables -j DROP rule. iptables -L shows it, at the top of the chain. Your application still says the other machine is reachable. Someone asked exactly that on unix.stackexchange (question 762671, 2023-11-29, "Ubuntu 22.04 iptables command not working", 11,225 views when we read it on 2026-10-25, no accepted answer). Two servers, three interfaces each, one DROP per pair, and this sentence:
"when I applied the rule, for a brief moment my app reports the neighbor server is unreachable and suddenly it will change to reachable, something is overriding the rule, am unsure."
Nothing overrides the rule. We rebuilt the situation on a Debian 13 virtual machine and measured it. The rule works. It just isn't looking at the packet you think, and iptables -L hides the two columns that tell you.
01. The verdict in three lines
iptables -L without -v hides the interface (in) and the packet counter. Those two columns decide everything. If the counter of your DROP stays frozen while the traffic passes, the packet never touched your rule: it came in through another link, or something above the rule accepted it.
The only answer under the question (score 1, not accepted) says "On Ubuntu 22.04 nftables is used by default" and suggests going back to iptables-legacy with update-alternatives. We followed it. It fixes nothing: it adds a second firewall that your iptables no longer shows.
Three commands tell the truth: iptables -V, iptables -L -v -n, nft list ruleset.
02. The lab: one machine, two links
Everything ran on 2026-10-25 on one virtual machine, Debian 13.7, kernel 6.12.113+deb13-arm64, with the stock iptables v1.8.11 (nf_tables), the default on Debian 13. A server srv and a client cli live in two network namespaces, joined by two veth pairs (lab addresses). All rules are set inside srv, so your own firewall is not touched and you need no second machine.
Commands provided as is, without warranty: try them on a throwaway machine or a snapshot.
Starting state of the alternative, which we restore at the end:
starting state
$ update-alternatives --query iptables | grep -E '^(Status|Value):'
Status: auto
Value: /usr/sbin/iptables-nft
The lab itself. The </dev/null matters: a nc in the background that reads the terminal freezes.
$ sudo ip netns add srv
$ sudo ip netns add cli
$ sudo ip link add s1 netns srv type veth peer name c1 netns cli
$ sudo ip link add s2 netns srv type veth peer name c2 netns cli
$ sudo ip -n srv addr add 10.25.1.1/24 dev s1; sudo ip -n cli addr add 10.25.1.2/24 dev c1
$ sudo ip -n srv addr add 10.25.2.1/24 dev s2; sudo ip -n cli addr add 10.25.2.2/24 dev c2
$ for l in s1 s2 lo; do sudo ip -n srv link set $l up; done; for l in c1 c2 lo; do sudo ip -n cli link set $l up; done
$ sudo ip netns exec srv nc -lk 9000 </dev/null >/dev/null 2>&1 &
nc -z -w 2 prints exit=0 when the connection succeeds and exit=1 when it times out after 2 seconds, which is what a DROP looks like. The control, no rule yet:
control: no rule
$ sudo ip netns exec cli nc -z -w 2 10.25.1.1 9000; echo exit=$?
Connection to 10.25.1.1 9000 port [tcp/*] succeeded!
exit=0
$ sudo ip netns exec cli nc -z -w 2 10.25.2.1 9000; echo exit=$?
Connection to 10.25.2.1 9000 port [tcp/*] succeeded!
exit=0
03. The rule, and what -L hides
Same shape as the asker's, inserted at the top. It blocks.
the rule from the question
$ sudo ip netns exec srv iptables -I INPUT -s 10.25.1.2 -d 10.25.1.1 -i s1 -j DROP
$ sudo ip netns exec cli nc -z -w 2 10.25.1.1 9000; echo exit=$?
exit=1
The list, then the list with -v:
iptables -L, then -v
$ sudo ip netns exec srv iptables -L INPUT -n
Chain INPUT (policy ACCEPT)
target prot opt source destination
DROP all -- 10.25.1.2 10.25.1.1
$ sudo ip netns exec srv iptables -L INPUT -v -n
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
2 120 DROP all -- s1 * 10.25.1.2 10.25.1.1
The first listing says "drop 10.25.1.2 to 10.25.1.1", full stop. The second says: only on s1, two packets so far. The iptables man page: -v "makes the list command show the interface name, the rule options (if any), and the TOS masks. The packet and byte counters are also listed".
04. Same source, same destination, other link
We force the client to reach 10.25.1.1 through its other link, c2, keeping the same source address 10.25.1.2. ip route get proves the path. The route stays in place: section 05 needs it.
same source, other link
$ sudo ip -n cli route add 10.25.1.1/32 dev c2 src 10.25.1.2
$ sudo ip netns exec cli ip route get 10.25.1.1
10.25.1.1 dev c2 src 10.25.1.2 uid 0
cache
$ sudo ip netns exec srv sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.s2.rp_filter
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.s2.rp_filter = 2
$ sudo ip netns exec cli nc -z -w 2 10.25.1.1 9000; echo exit=$?
Connection to 10.25.1.1 9000 port [tcp/*] succeeded!
exit=0
$ sudo ip netns exec srv iptables -L INPUT -n
Chain INPUT (policy ACCEPT)
target prot opt source destination
DROP all -- 10.25.1.2 10.25.1.1
$ sudo ip netns exec srv iptables -L INPUT -v -n
Chain INPUT (policy ACCEPT 4 packets, 216 bytes)
pkts bytes target prot opt in out source destination
2 120 DROP all -- s1 * 10.25.1.2 10.25.1.1
Reached, with the rule in place. The DROP counter is frozen at 2, and the policy counter went from 0 to 4. The packets went around the rule and fell on the policy.
Why does the packet arrive at all? Linux accepts a packet addressed to any of its own addresses, whatever interface it came in on. RFC 1122 section 3.3.4.2 describes the two models and says "(A) A host MAY silently discard an incoming datagram whose destination address does not correspond to the physical interface through which it is received". The RFC doesn't say which model Linux follows. Our bench does: it accepts. Here rp_filter is 0 for all and 2 (loose) for s2, so the kernel did not drop it on source validation either. Strict mode (rp_filter=1) might drop this packet. We did not measure that.
A rule says "from this source, on this interface". The same source on the other interface is another packet.
05. The three commands, then the fix
iptables -V says which engine you talk to. nft list ruleset is the judge: it shows everything the kernel applies, whichever tool put it there.
iptables -V, nft list ruleset
$ sudo ip netns exec srv iptables -V
iptables v1.8.11 (nf_tables)
$ sudo ip netns exec srv nft list ruleset
table ip filter {
chain INPUT {
type filter hook input priority filter; policy accept;
ip saddr 10.25.1.2 ip daddr 10.25.1.1 iifname "s1" counter packets 2 bytes 120 drop
}
}
Your iptables rule is an nftables rule, counter and iifname "s1" included. The xtables-nft man page says it: "Adding a rule will result in that rule being added to the nf_tables kernel subsystem instead." So "nftables is the default, so iptables doesn't work" is the wrong diagnosis: iptables-nft filters, from our first test.
The fix: block the neighbour by address, all its addresses, with no -i. We flush first, keep the route through c2 to prove the crossed path, then remove it.
the fix: by address, no -i
$ sudo ip netns exec srv iptables -F INPUT
$ sudo ip netns exec srv iptables -I INPUT -s 10.25.1.2 -j DROP
$ sudo ip netns exec srv iptables -I INPUT -s 10.25.2.2 -j DROP
$ sudo ip netns exec cli nc -z -w 2 10.25.1.1 9000; echo exit=$?
exit=1
$ sudo ip netns exec cli ip route get 10.25.1.1
10.25.1.1 dev c2 src 10.25.1.2 uid 0
cache
$ sudo ip -n cli route del 10.25.1.1/32 dev c2
$ sudo ip netns exec cli nc -z -w 2 10.25.1.1 9000; echo exit=$?
exit=1
$ sudo ip netns exec cli nc -z -w 2 10.25.2.1 9000; echo exit=$?
exit=1
$ sudo ip netns exec srv iptables -L INPUT -v -n
Chain INPUT (policy ACCEPT 4 packets, 216 bytes)
pkts bytes target prot opt in out source destination
2 120 DROP all -- * * 10.25.2.2 0.0.0.0/0
4 240 DROP all -- * * 10.25.1.2 0.0.0.0/0
Everything is blocked, the crossed path included, and both counters moved. Two more results, measured in another pass, without blocks. A rule added with -A behind an ACCEPT never sees anything: on our bench the ACCEPT on line 1 counted 4 packets and the DROP on line 2 counted 0. And a DROP inserted at the top also cuts a connection that is already open: a continuous ping got 10 replies out of 30, cut dead, and a TCP stream was cut the same way.
iptables -Z zeroes the counters before a test, so you see which one moves. If you really need the interface in the rule, write one rule per interface and per address (not benched).
06. "Revert to iptables-legacy": what it really does
We did what that answer says, with the two DROP rules still in place:
revert to iptables-legacy
$ sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
update-alternatives: using /usr/sbin/iptables-legacy to provide /usr/sbin/iptables (iptables) in manual mode
$ sudo ip netns exec srv iptables -V
iptables v1.8.11 (legacy)
$ sudo ip netns exec srv iptables -L INPUT -v -n
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
$ sudo ip netns exec cli nc -z -w 2 10.25.1.1 9000; echo exit=$?
exit=1
The list is empty and the traffic is still blocked. The nftables rules are still there, iptables just can't see them. We added a legacy ACCEPT on top, hoping to let it through:
a legacy ACCEPT on top
$ sudo ip netns exec srv iptables -I INPUT -j ACCEPT
$ sudo ip netns exec cli nc -z -w 2 10.25.1.1 9000; echo exit=$?
exit=1
$ sudo ip netns exec srv iptables -L INPUT -v -n
Chain INPUT (policy ACCEPT 2 packets, 120 bytes)
pkts bytes target prot opt in out source destination
2 120 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0
$ sudo ip netns exec srv iptables-nft -L INPUT -v -n
# Warning: iptables-legacy tables present, use iptables-legacy to see them
Chain INPUT (policy ACCEPT 4 packets, 216 bytes)
pkts bytes target prot opt in out source destination
2 120 DROP all -- * * 10.25.2.2 0.0.0.0/0
8 480 DROP all -- * * 10.25.1.2 0.0.0.0/0
Still blocked. A packet crosses both firewalls, legacy and nftables, and an ACCEPT on one side does not save it from a DROP on the other. The legacy ACCEPT counted 2 packets, the nftables DROP went from 4 to 8. The only hint is the # Warning line printed by iptables-nft. Each tool shows only its own side.
We came back with --auto, not --set: it restores exactly the starting state (Status: auto), not a manual choice.
back with --auto
$ sudo ip netns exec srv iptables -F INPUT
$ sudo update-alternatives --auto iptables
update-alternatives: using /usr/sbin/iptables-nft to provide /usr/sbin/iptables (iptables) in auto mode
$ sudo ip netns exec srv iptables -V
iptables v1.8.11 (nf_tables)
On a Raspberry Pi, the advice can't even be applied
Same bench on a Raspberry Pi Zero 2 W under Debian 13.7, kernel 6.18.50+rpt-rpi-v8. The iptables-nft results are identical to the virtual machine's. Legacy fails:
Raspberry Pi: iptables-legacy
$ sudo ip netns exec srv iptables-legacy -I INPUT -s 10.25.1.2 -d 10.25.1.1 -i s1 -j DROP
modprobe: FATAL: Module ip_tables not found in directory /lib/modules/6.18.50+rpt-rpi-v8
iptables v1.8.11 (legacy): can't initialize iptables table `filter': Table does not exist (do you need to insmod?)
On that kernel, "revert to iptables-legacy" is not a fix you can apply.
07. The asker's case, the scope, the teardown
Their symptom, unreachable for a moment and then reachable, fits section 04: an application that switches to another link between the two servers, and rules bound to one interface and one source. With three interfaces per machine it is the most plausible hypothesis. It stays a hypothesis. We did not reproduce the brief cut, and their real cause is unknown to us.
To settle it on their servers: iptables -L -v -n before the test and after, iptables -Z in between, and watch which counter moves. Their OUTPUT rules with -o follow the same logic, unbenched.
Measured: the Debian 13.7 virtual machine (kernel 6.12.113+deb13-arm64) and the Raspberry Pi Zero 2 W (kernel 6.18, Raspberry Pi build). Not measured: Ubuntu 22.04 (same iptables-nft tool, but we did not test it), rp_filter in strict mode, OUTPUT rules, and the real cause on the asker's servers.
The teardown, measured: no namespace, no veth, the alternative back in auto, and no legacy table left on the machine (the last line counts the Warning, 0).
$ sudo ip netns pids srv | xargs -r sudo kill
$ sudo ip netns del srv; sudo ip netns del cli
$ sudo ip netns list | wc -l
0
$ ip -br link | grep -c -E '^(s|c)[12]@' ; true
0
$ update-alternatives --query iptables | grep -E '^(Status|Value):'
Status: auto
Value: /usr/sbin/iptables-nft
$ sudo iptables -L -n 2>&1 | grep -c Warning ; true
0
The same gap, between what a tool lists and what the kernel does, turns up in a fail2ban that says the IP is banned while it still connects and in a ufw that says active with no rule loaded.