Why I Needed SMTP-Only VPN Routing
I run email-based automation workflows that occasionally get flagged by spam filters or rate-limited by certain SMTP servers. To test how different IP addresses affect deliverability and to work around temporary blocks, I needed a way to route only SMTP traffic through a WireGuard VPN while keeping everything else on my regular connection.
Full VPN tunneling wasn't an option—it would slow down my entire network and break local services. I needed surgical routing: SMTP packets go through the VPN, everything else stays local.
My Setup
I run this on a Proxmox VM (Ubuntu 22.04) that handles my email automation stack. The VM sits on my home network and connects to a WireGuard server I rent from a VPS provider.
The goal was simple: route traffic destined for port 25, 465, and 587 (SMTP/SMTPS/submission) through the WireGuard tunnel, while DNS queries, web traffic, and local network communication stay untouched.
Installing WireGuard
WireGuard was already available in Ubuntu's default repositories:
sudo apt update
sudo apt install wireguard wireguard-tools
I generated a client keypair directly on the VM:
wg genkey | tee privatekey | wg pubkey > publickey
I sent the public key to my VPS provider, who added it to their WireGuard server config and gave me back their server's public key, endpoint IP, and my assigned VPN IP address.
Basic WireGuard Configuration
I created /etc/wireguard/wg0.conf with this initial setup:
[Interface]
PrivateKey = <my_private_key>
Address = 10.8.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = <server_public_key>
Endpoint = <vps_ip>:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
This configuration routes all traffic through the VPN, which I didn't want. The AllowedIPs = 0.0.0.0/0 directive tells WireGuard to send everything over the tunnel.
I tested this full-tunnel setup first to confirm the VPN worked:
sudo wg-quick up wg0
curl ifconfig.me
The IP returned was my VPS's address, not my home ISP. Good—the tunnel worked. But now I needed to restrict it to SMTP only.
What Didn't Work: Simple AllowedIPs Changes
My first attempt was to replace AllowedIPs with the IP addresses of the SMTP servers I was testing against:
AllowedIPs = 192.0.2.10/32, 198.51.100.5/32
This worked for those specific IPs, but it was fragile. Every time I tested a new mail server, I had to update the config and restart the tunnel. It also didn't handle dynamic IPs or DNS-based SMTP endpoints.
I needed a solution that routed traffic based on port, not destination IP.
The Solution: Policy-Based Routing with iptables
WireGuard itself doesn't support port-based routing. The AllowedIPs field only understands IP ranges, not ports.
To route by port, I used Linux policy routing with a custom routing table and iptables packet marking.
Step 1: Create a Custom Routing Table
I added a new routing table to /etc/iproute2/rt_tables:
200 vpn_smtp
This table would hold the route that sends marked packets through the WireGuard interface.
Step 2: Modify WireGuard Config
I changed my wg0.conf to prevent WireGuard from taking over all routing:
[Interface]
PrivateKey = <my_private_key>
Address = 10.8.0.2/32
Table = off
[Peer]
PublicKey = <server_public_key>
Endpoint = <vps_ip>:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
The Table = off directive tells WireGuard not to automatically add routes. I would manage routing manually.
Step 3: Add PostUp Commands for Routing
I added these lines to the [Interface] section:
PostUp = ip route add default dev wg0 table vpn_smtp
PostUp = ip rule add fwmark 0x1 table vpn_smtp
PostUp = iptables -t mangle -A OUTPUT -p tcp --dport 25 -j MARK --set-mark 1
PostUp = iptables -t mangle -A OUTPUT -p tcp --dport 465 -j MARK --set-mark 1
PostUp = iptables -t mangle -A OUTPUT -p tcp --dport 587 -j MARK --set-mark 1
PostDown = ip rule del fwmark 0x1 table vpn_smtp
PostDown = ip route del default dev wg0 table vpn_smtp
PostDown = iptables -t mangle -D OUTPUT -p tcp --dport 25 -j MARK --set-mark 1
PostDown = iptables -t mangle -D OUTPUT -p tcp --dport 465 -j MARK --set-mark 1
PostDown = iptables -t mangle -D OUTPUT -p tcp --dport 587 -j MARK --set-mark 1
Here's what each command does:
ip route add default dev wg0 table vpn_smtp— Creates a default route in the vpn_smtp table that sends traffic through wg0.ip rule add fwmark 0x1 table vpn_smtp— Routes packets marked with 0x1 using the vpn_smtp table.iptables -t mangle -A OUTPUT -p tcp --dport 25 -j MARK --set-mark 1— Marks outgoing TCP packets destined for port 25 with 0x1.
The same marking happens for ports 465 and 587.
Testing the Configuration
I brought up the tunnel:
sudo wg-quick up wg0
Then I verified the routing table and firewall marks:
ip route show table vpn_smtp
ip rule show
sudo iptables -t mangle -L OUTPUT -n -v
The first command showed the default route through wg0. The second showed the fwmark rule. The third listed the packet marking rules for SMTP ports.
To confirm SMTP traffic went through the VPN, I used tcpdump on the WireGuard interface while sending a test email:
sudo tcpdump -i wg0 -n port 25
I saw packets flowing. When I checked my regular interface (eth0), SMTP traffic was absent. Non-SMTP traffic (like HTTP) still appeared on eth0.
I also tested my public IP for non-SMTP traffic:
curl ifconfig.me
It returned my home ISP address, not the VPN IP. Good.
What Broke Initially
The first version of my PostUp commands had a typo in the iptables rules—I accidentally used -A PREROUTING instead of -A OUTPUT. PREROUTING applies to forwarded packets, not locally generated ones. Since my SMTP traffic originated from the VM itself, the rule never matched.
I caught this by running:
sudo iptables -t mangle -L -n -v
The packet counters for my PREROUTING rule stayed at zero. Once I switched to OUTPUT, packets started getting marked.
Another issue: I forgot to set Table = off in the WireGuard config initially. WireGuard was still installing its own default route, which overrode my custom table. All traffic went through the VPN until I disabled automatic route management.
Limitations and Trade-offs
This setup works for my use case, but it has constraints:
- It only routes outbound SMTP. If I needed inbound SMTP (running a mail server), I'd need DNAT rules as well.
- The iptables rules apply system-wide. If I had multiple processes using SMTP, they'd all route through the VPN. I didn't need per-process control, but if you do, you'd need cgroup-based marking.
- DNS for SMTP hostnames still resolves over my regular connection. If the SMTP server's IP changes, the tunnel still works, but if DNS itself is blocked or manipulated, this won't help.
- This doesn't protect against traffic analysis. An observer on my local network can still see I'm making SMTP connections, just not where they're going.
Key Takeaways
WireGuard's simplicity is both a strength and a limitation. It doesn't natively support port-based routing, so you have to layer policy routing and packet marking on top of it.
The Table = off directive is critical for split tunneling. Without it, WireGuard takes over all routing, and your custom rules get ignored.
Testing with tcpdump and checking iptables counters saved me hours of guessing. If packets aren't flowing where you expect, verify your rules are actually matching traffic.
For my email testing needs, this setup works reliably. SMTP traffic exits through the VPN, everything else stays local, and I can toggle the tunnel on and off without disrupting other services.