The host keeps answering SSH after a full-tunnel VPN comes up, and a Docker-published service on the same machine goes silent. Nothing is wrong with the listener: the container is running, the port mapping exists, and the packet arrives. The container is healthy and the request does reach it. What differs is where the reply starts from when Linux chooses its route, and that difference decides which of the two designs below you need.

Follow the tuple through destination NAT

Take a host at 192.0.2.10 publishing TCP 2222 to a container at 172.30.240.5:22. A request from 198.51.100.25 is translated before it reaches the container, and the container answers as itself.

WAN request:      198.51.100.25 -> 192.0.2.10:2222
after DNAT:       198.51.100.25 -> 172.30.240.5:22
container reply:  172.30.240.5:22 -> 198.51.100.25

At the host's route decision the reply still carries the container's source address. Reverse translation restores the published tuple later, which is why the packet looks wrong at exactly the moment the route is chosen.

Why a host-only rule misses it

A rule that selects the WAN table only for packets from 192.0.2.10 covers the host's own daemons and misses this forwarded reply entirely. The source address on the reply is 172.30.240.5, not the host.

The over-correction is to route everything from the container through the WAN, which also exempts the container's unrelated outbound connections from the VPN. That is a different policy than the one you were asked for, and it usually surprises someone later.

Post-DNAT matching is a different hook

Docker's firewall documentation notes that ordinary user filtering sees the post-DNAT destination, so matching the published port 2222 in a normal rule is not the same as matching container port 22. If the original tuple matters, use the supported original-destination mechanism rather than guessing which port is visible.

This hook confusion produces rules that look correct and never match. Check the counters on the rule itself before rewriting the policy. A rule with zero hits is telling you it is being evaluated somewhere the packet never reaches.

Design A: carry the decision as connection state

A connection mark can carry the ingress decision across the translation, but it has to be restored to the packet before the route lookup that matters. The stages are:

  1. Reserve a mark field and rule priority that do not collide with Docker, the VPN or another policy owner.
  2. Identify only the intended new WAN connection at a pre-DNAT point where connection tracking is available.
  3. Record the decision on the connection mark, without forcing the original request itself onto the WAN table.
  4. On the reply direction, restore only your own mark bits to the packet before routing.
  5. Select a complete WAN table early enough that a VPN rule cannot win first.

That table needs the real connected routes and gateway. It also needs a defined behaviour when the route is missing. Silent fall-through into the tunnel is the worst outcome, because the configuration still looks like it works. Fail closed and log it instead.

Design B: terminate on the host

A deliberately configured host-side frontend, such as a TCP proxy or a reverse proxy in front of the container, keeps the reply originating from the host address. Rules written for the host then behave as expected, and the container stays on its private network.

The cost is another moving part with its own configuration, source-address handling and access controls. It is usually the simpler supported boundary, and it is often the right answer when the routing policy would be maintained by one person who might leave.

Verify the change, not the intent

Open a fresh authorized connection from the public side and confirm the reply leaves the intended interface. Then check that an unrelated container-initiated request still follows the VPN.

Do not flush the machine's connection table to test one rule: existing entries keep their earlier mapping, and the flush breaks everything else on the host. Publishing a port defines reachability. It does not define the reply path. Write the chosen design down with its table names, so the next person does not rediscover it during an incident.

ХӀокхунах тера йаззамаш