A service published through a gateway works from most of the internet and fails from one office. Someone checks the tunnel, sees the request arrive, and concludes the reply must be following the same path back. Linux does not work that way, and no amount of inspecting the tunnel will change the answer. The reply is routed by destination, and that one failing caller sits on a subnet the host already reaches directly.

What actually decides an outgoing packet

The kernel looks up a route for the packet's destination, consulting policy rules in priority order and then the table a matching rule selects. The interface a packet arrived on plays no part unless a rule or a mark was written to make it matter. That is a routing fact about this host, not a fault in the tunnel.

A request received on the tunnel can therefore produce a reply that leaves through the physical interface, and the flow turns one-way without any device reporting an error. Check both directions before drawing a conclusion about the tunnel. A reply that leaves the wrong way disappears from the tunnel counters, which is why the two ends disagree about whether anything was sent. Note which counter moved before touching a rule.

Why the local subnet wins

A host with an address on a local network holds a connected route for that prefix in the main table, and a connected route is a direct match. When the caller comes from that same subnet, the lookup finds the local link before the tunnel is ever considered.

The asymmetry is what makes this confusing to diagnose. Callers from the wider internet work because they match no local route, while the one caller sharing the office prefix fails, which is exactly the caller somebody tests with.

Peer coverage is not the same as installed routes

On the tunnel side, a peer's allowed ranges describe what may be sent into the tunnel and which sources are accepted from it. They say nothing about where the local routing table will send a packet.

If the tunnel tool installs a default route on the host, it can drag unrelated traffic into the tunnel as collateral damage. Where a policy rule should own the decision instead, the tool's route-installation option exists to stop it doing so, and the addresses, keys and endpoint settings stay exactly as they were. A default route added for convenience is a common way for a host to lose its own local network.

Select the table by the source address

A source-based rule sends replies from the service address into a private table whose default is the tunnel, while everything else keeps using the main table. Pick table and priority numbers that are unused locally, and read the existing rules before inserting anything. Read the live rules rather than a saved file, because the two can differ after a reboot.

ip -6 route add default dev wg0 table 200
ip -6 rule add priority 32000 from 2001:db8:aa::20/128 lookup 200
ip -6 rule add priority 32001 from 2001:db8:aa::20/128 unreachable

The last line makes a missing route fail visibly rather than falling back to the local link. The source must genuinely be assigned to the host, and the ordinary local and main lookups must stay in their earlier positions. The rule chooses a table by source; it does not compare prefix lengths across tables.

Confirm the path, then the service

Ask the kernel what it would do instead of inferring from a capture, using the service address as the source selector. A wrong answer here saves a whole packet trace.

ip -6 route get 2001:db8:bb::30 from 2001:db8:aa::20

The output should name the tunnel. Then test a source you did not touch and confirm it still takes the local link, so the change stayed scoped to one address. A wider selector would prove nothing about the original complaint.

What this does not fix

Keep forwarding, firewall rules and ICMPv6 behaviour on the gateway as they were, because a source route replaces none of them. A missing Packet Too Big message still breaks large replies no matter which table won the lookup. Fix that before adding routing rules, or the symptom will survive the change.

Confirm the publication model before applying any of this. A reverse proxy that opens its own backend connection presents its own source and does not preserve the caller, so this fault cannot occur in that design. Applying the rule there would add a routing exception that solves nothing and hides the real cause.

Finally, write the rule numbers and the table into the host's configuration management. A policy rule added by hand disappears at the next rebuild, and the office caller comes back with the same complaint.

Pennadoù liammet