Four things have to line up before a Linux host sends only its own SMTP out through a tunnel: a route in a dedicated table, a policy rule that selects the traffic, a source address the peer will accept, and a return path that survives the asymmetry. Each one is a separate configuration with its own failure mode. The order below is the one that makes a failure point at the piece that is actually wrong. It is a port-specific routing job, not a request for a VPN default route.

A Port Rule Is Not a Default Route

A rule matching locally generated TCP with destination port 25 sends that traffic to a table you name. The rule creates nothing by itself: it selects, and the table has to hold a route that can carry the packet. Combining the two covers only the outbound direction of one protocol, so forwarding for other hosts, inbound mail connections and IPv6 stay outside the arrangement. Calling it a port-specific policy rather than a VPN default keeps that boundary honest.

Two wg-quick settings are prerequisites rather than part of the policy. AllowedIPs = 0.0.0.0/0 lets the peer carry arbitrary IPv4 destinations, and Table = off stops wg-quick from installing its own default route for that prefix. Do not mix these instructions with a manager that applies its own route policy, because the two will disagree about who owns the main table.

Write the Route, the Selector and the Guard

Pick a table number and a pair of priorities that nothing else uses, then apply the route, the selector and the guard in that order. The numbers below are examples, not reserved values:

ip -4 route add default dev wg0 src 10.77.0.2 table 51825
ip -4 rule add priority 10000 iif lo ipproto tcp dport 25 lookup 51825
ip -4 rule add priority 10001 iif lo ipproto tcp dport 25 unreachable

iif lo restricts the rule to locally originating traffic, and ipproto tcp dport 25 identifies the direction of the connection; the ip-rule reference documents both selectors. They exist on current kernels and iproute2 releases, and on an older system the rule is rejected outright, which is a clearer failure than one that loads and matches nothing.

The unreachable rule at the next priority is the guard. Without it, a missing route in table 51825 falls through to the ordinary default, and mail leaves over the physical link during a tunnel outage, which is the exact case the policy was built to prevent.

The Source Address Has to Agree

The route's src preference only fills in a source for connections that have not bound one of their own. It is not translation, and it does not override an explicit bind, so a daemon bound to the physical address keeps sending from that address. If that is what you find, change the daemon's own setting or design a deliberate translation policy, because a late blanket masquerade is a different solution with different consequences.

At the gateway, the client's peer has to authorise the source it actually receives, which is 10.77.0.2/32 rather than a broad destination prefix. Copying the client's 0.0.0.0/0 into every gateway peer confuses which peer owns which traffic. The gateway also needs forwarding, a firewall policy that permits the flow, and a return path, including egress translation when the upstream cannot route a private tunnel address.

Check the Return Path, Then Persistence

Read the lookups before trusting anything, because they show which table and which source each direction gets. One destination on port 25 and the same destination on port 443 are enough:

ip -4 route get 198.51.100.25 ipproto tcp dport 25
ip -4 route get 198.51.100.25 ipproto tcp dport 443

The first lookup should name wg0 and the intended source, and the second should keep the ordinary uplink. When the application binds its own address, add from with that address to the lookup. A reply direction that reaches wg0 and is then dropped locally points at IPv4 reverse-path filtering, because a port-only outgoing rule does not make the reverse lookup choose the tunnel.

Where asymmetric routing is authorised, interface-scoped loose mode through net.ipv4.conf.wg0.rp_filter=2 may be appropriate, and it is worth checking the effective maximum of the all and interface values first. Do not disable validation globally as a shortcut. Then hand the table, the priorities and the guard to whatever owns the configuration, and open fresh connections after a reconnect to confirm the selection still holds. Keep in mind that replies from a local SMTP listener carry source port 25 rather than destination port 25, which is a separate policy question.

Artihkkalat mat laktásit