mabel.ink

A firewall rule can do nothing on one protocol and everything on the other

July 25, 2026

I came to Linux firewalls from Windows, where the host firewall sees everything. If a port is listening on a Windows server, Windows Firewall has an opinion about it, and a block rule blocks it. I carried that assumption onto my home server, and it turned out to be wrong in a way that only shows up once you test from another machine.

The assumption

The server runs UFW with a default deny for incoming traffic, and most of my services run in Docker containers. So the mental model was simple: UFW denies everything, I allow what I want, and a container port with no allow rule is closed.

It is not, and I only found that out by testing from another machine on the network instead of reading the rule list.

Why IPv4 skips the firewall

When a compose file publishes a port, Docker does it with a NAT rule. Traffic arriving for that port gets rewritten (DNAT) and forwarded to the container. That traffic is not destined for the host itself, it is being routed through it, so it travels the FORWARD path in iptables.

UFW's rules live in the INPUT chain, which only sees traffic addressed to the host. Published container traffic never goes there. The firewall is simply never consulted.

The practical result on IPv4:

If you genuinely need to close one, you bind it to loopback in the compose file or put a rule in the DOCKER-USER chain, which is evaluated before Docker's own rules.

Host-networked containers are different. They bind directly on the host with no NAT in between, so their traffic does hit INPUT and UFW governs them normally. Same Docker, two completely different firewall behaviors, depending on one line in the compose file.

Why IPv6 does not

Here is the part I did not expect. On this host the Docker bridge networks are IPv4 only. Docker creates the ip6tables NAT chain for Docker but never puts a rule in it. You can see that for yourself:

ip6tables -t nat -S DOCKER

On my box that prints one line, the chain declaration, and nothing else.

With no IPv6 NAT, something still has to answer connections to the published port over IPv6. That job falls to docker-proxy, a userland process that listens on the host like any other program. Its traffic is ordinary traffic addressed to the host, so it does go through INPUT, and UFW governs it.

So the same published port behaves like this:

One rule, two protocols, opposite effects.

What that means in practice

A rule that looks like it protects a port may do nothing on one protocol and everything on the other. If you only test over IPv4, you will conclude the rule is pointless and delete it. If you only read the rule list, you will conclude the port is protected. Both conclusions are wrong for half the traffic.

For me this decided what to keep. During a firewall cleanup I scoped every service to the local network instead of leaving broad allow rules. For the published ports, the new rules do nothing over IPv4. I kept them anyway, because over IPv6 they are the only thing at the host layer, and before the cleanup the broad versions of them left those services open on a globally routable address at that layer. My notes say so in as many words: the rule is cosmetic on one side and load-bearing on the other.

The lesson for a Windows admin

Windows Firewall sits on the host's network stack and filters at the point where traffic is delivered to a process. It does not matter how the packet got there.

On Linux the firewall is a set of chains, and which chain a packet traverses depends on its destination and how it was handled on the way in. Traffic for the host goes through INPUT. Traffic being routed through the host goes through FORWARD. Docker deliberately puts published ports on the second path. UFW is a front end for the first.

So "is there a firewall rule for this port" is the wrong question. The right ones are:

Two more habits came out of the same exercise. Re-read ufw status after every change, because the command output lied to me once: a delete reported failure while having succeeded. And treat the rule list as a claim to be checked, not evidence.

One caveat I will state plainly: the IPv6 behavior here follows from my networks being IPv4 only. That is what my notes record, and I have not tested how it changes with a different Docker network or daemon configuration. Check your own setup before trusting mine.