Nothing in the socket remembers which interface carried the request, so a full-tunnel VPN default can capture a reply that belongs on the WAN. The service is not ignoring the inbound interface; Linux routes a locally generated reply by destination and source policy. A source-based rule can send only the service's replies through a complete WAN table instead, leaving ordinary egress on the tunnel. The port stays open while the answer leaves through the wrong path, which is why the symptom looks one-sided.

The misconception: replies follow the interface they arrived on

It feels logical that a packet arriving on eth0 should leave on eth0. Stateless packet filters can be written that way, and tcpdump on the inbound side shows the request exactly where expected. Routing does not work that way for traffic the host originates.

Once the service accepts the connection, its reply is a new locally generated packet. The kernel consults the routing rules for the reply's destination and source, and the VPN's default route is a perfectly valid answer. Nothing in the socket remembers which interface carried the request.

That is why the symptom looks one-sided: requests arrive, replies vanish. It is also why adding a firewall rule for the inbound port changes nothing. The packet is not being blocked on the way in; it is being routed somewhere else on the way out. Follow the reply with a source-qualified lookup and the detour becomes visible.

What the kernel actually routes: the source address

The handle you need is the local address the service uses for replies. On a host with an illustrative eth0 address of 192.0.2.10/24 and gateway 192.0.2.1, that address is present on the interface. On a cloud VM, the public address may be translated upstream and absent from the guest, so use the address the host actually holds.

Inspect the current state before reserving a table number or rule priority. Existing policy rules can shadow a new one, and an old table with the same number may already carry unrelated routes:

ip -4 addr show dev eth0
ip -4 rule show
ip -4 route show table all

Then select a table by source. The rule matches replies that already carry 192.0.2.10, which is exactly the locally terminated traffic you want to exempt. Forwarded traffic and packets from other local addresses do not match.

Build a complete return table, not half a default

A table with only a default route is not enough. The kernel still has to resolve the gateway, and that resolution needs the connected prefix in the same table. A table missing that prefix fails at gateway resolution even when its default route looks correct. Add both, then point the source rule at the table:

ip -4 route add table 100 192.0.2.0/24 dev eth0 src 192.0.2.10
ip -4 route add table 100 default via 192.0.2.1 dev eth0
ip -4 rule add priority 1000 from 192.0.2.10/32 lookup 100

Use the real prefix and gateway rather than copying the example. A cloud interface configured as a /32 with an on-link gateway needs a different connected route, and a table built from the wrong prefix fails at the first hop. The src attribute states the preferred source for that route; it does not replace the source rule.

Order matters too. A broad rule with a lower priority number is evaluated first, so the source rule must be reached before any rule that would send this traffic back to the main table. Check the priorities with ip rule show after adding it, not only at boot.

Verify with a source-qualified lookup

Ask the kernel to route a test destination from the service address. The answer should name eth0 and the WAN gateway, not the tunnel:

ip -4 route get 198.51.100.25 from 192.0.2.10

Then make a fresh authorized connection and watch both directions. A lookup that selects the right table while the live connection still fails points at reverse-path filtering or a firewall rule, not at the table. Check those controls before disabling anything globally.

Put the final routes and rule under the network manager that owns the interface. Address changes, DHCP renewal and VPN reconnects all rewrite kernel state, and a rule added by hand disappears or points at a stale gateway after the next event.

Where this exception stops

The rule exempts locally terminated traffic from one source address. It does not cover Docker-published ports, DNAT, or services forwarded through the host, because those packets are routed as forward traffic and never carry the host's local source in the same way. Those designs usually need a mark set on ingress and a rule that matches the mark.

It also does not make the host's public address appear on the interface. When an upstream NAT owns that address, the reply still has to leave through the path that NAT expects, which is why the WAN table has to mirror the original gateway and prefix rather than a guessed public range.

Keep the exception documented with its table number, rule priority and the exact source address. The next change to the VPN or the interface can silently break it, and a note that names the three values is faster to check than rediscovering the symptom from a customer report.

If the service still answers on the wrong path after the source-qualified lookup passes, compare the live socket's source address with the one in the rule. A service bound to a wildcard address can choose a different source than the address you tested, and that mismatch is the remaining gap.

Mikanda mikatakata