Nested WireGuard: the middle hop cannot answer its own port
When the middle hop sends everything into one tunnel, replies to its inbound service leave by the wrong path. A source-port rule at the interface fixes it.
Three WireGuard hops reach a network that has no public address, and then the middle machine stops answering on the port it was supposed to listen on. The tunnel onward still works, the far host still responds, and only the middle server itself has gone quiet from the outside. That combination is confusing precisely because it is self-inflicted: the middle hop is obeying its own routing table, and the table sends its answers the wrong way.
A chain that works until you talk to the middle box
Take an illustrative chain: client A reaches server B on UDP 500, and B reaches onward to server C on UDP 500. C accepts traffic only from its single peer address, which makes it a leaf and keeps its behaviour simple. B is not a leaf, and everything interesting about this failure happens there.
The distinction matters because leaf and middle roles pull in opposite directions. A leaf can send everything into one tunnel without consequence, since nothing else depends on it. A middle hop is simultaneously an endpoint for clients and a client toward the far side, and those two jobs want different routing.
Why the reply leaves by the tunnel
On B, the interface facing C carries AllowedIPs = 0.0.0.0/0 together with the matching default route. Every packet B originates therefore goes into that tunnel unless something more specific takes precedence.
A packet from A addressed to B arrives and is accepted, because B is the destination and local delivery happens first. The reply is a brand-new locally generated packet aimed at A's public address, and that destination matches the default route into the tunnel. C receives it, does not own that address, and drops it.
The symptom reads as a firewall problem or a key mismatch, and neither is true here. A capture on the tunnel toward C shows the replies heading the wrong direction. That single observation saves more time than any amount of reading configuration, so write it down before you start changing keys. Anyone who has not seen the capture will assume the tunnel itself is broken.
The narrow repair: policy rules that match a source port
Replies from B's listening socket leave with source port 500. A policy rule can match that and route those packets back to the ordinary table:
ip rule add sport 500 table main priority 100
ip rule add sport 8080 table main priority 101
Policy rules are consulted in priority order, and the main table sits at 32766, so a rule at 100 is asked before the tunnel default route gets a say. Add one entry per port that must remain reachable from outside and nothing more, so forwarded traffic keeps using the tunnel it was built for.
Know the boundary of this trick. The sport selector matches locally generated packets, which covers exactly the reply case and deliberately does not cover traffic B merely forwards for somebody else. A service hosted behind B still needs its own answer, and pretending otherwise produces a fix that works for the first service and fails for the second.
Give the rule an owner
A rule typed at a prompt disappears the next time the interface is rebuilt. On a WireGuard setup, put it where the interface is created and removed:
PostUp = ip rule add sport 500 table main priority 100
PostDown = ip rule del sport 500 table main priority 100
If NetworkManager, a distribution hook or a container entrypoint owns the interface, the pair belongs in that owner rather than in a hand-edited file. Check the effective rules after a reconnect, not only after the first start, because the first start is the case that always works.
Compare the alternatives honestly
The source-port rule is one of four ways out, and it is the narrowest of them. It has the advantage of not touching the tunnel policy at all, which is why it tends to survive later changes. Narrow fixes also fail loudly when they stop applying, whereas a broad exception keeps working quietly long after the reason for it disappeared. That difference matters more than elegance when someone else owns the box in two years.
| Approach | What it costs | Where it breaks |
|---|---|---|
| Source-port policy rule | One rule per exposed port | Anything B forwards rather than originates |
| Destination exception route | A route pinned to specific client addresses | Clients whose address changes |
| Stop full-tunnelling on B | Loses the catch-all path toward C | Designs that depend on it |
| Split into two tunnels | More interfaces and more policy | Nothing, at the cost of complexity |
Verify from both ends once you have chosen. From A, start a connection to B's port and keep it busy for a while. On B, make the kernel justify the reply path with something like ip route get <address-of-A> sport 500, and confirm it names the WAN interface rather than the tunnel. After that, check that something behind C still answers through the chain, which proves the exception stayed narrow instead of turning into a bypass. Finally, record the ports in the design notes, or the next operator rediscovers this on a different service.