WireGuard on MikroTik
2025-12-27
For some time now, I’ve been mucking about with my home network setup, specifically trying to get multiple WireGuard tunnels working simultaneously on my MikroTik router. What started as a simple “I want to route some traffic here and some traffic there” turned into a proper deep dive into RouterOS routing tables, mangle rules, and the joys of CGNAT workarounds.
For the longest time I thought WireGuard was a simple “turn it on and forget about it” sort of thing. And to be fair, for basic use cases it is. But when you want to run multiple tunnels, do split tunneling, or set up always-on VPN whilst still accessing your home network remotely - well, that’s when things get interesting.
This post covers the advanced WireGuard configurations I’ve learnt whilst setting up my network. I’ll do my best to walk through multiple independent tunnels, split tunneling based on destination, always-on VPN with routing tables, and even a CGNAT relay setup for when you’ve got multiple devices behind carrier-grade NAT. All using CLI commands, because clicking through Winbox is a bit of a drag when you want to reproduce your config later.
Prerequisites
Before we dive in, this post covers a a MikroTik router running RouterOS 7.x, terminal access via SSH or Winbox, and the connection details from whatever WireGuard servers you’re connecting to. I’m assuming you’ve got a basic understanding of IP routing and firewall concepts - if you know what a routing table does and why NAT exists, you’ll be fine.
Multiple Interfaces
RouterOS supports multiple independent WireGuard interfaces, which is brilliant when you need different tunnels for different purposes. I’ve got one for my VPN provider and another for my mesh network between sites.
Creating interfaces is straightforward:
/interface/wireguard add name=wg0
/interface/wireguard add name=wg1
RouterOS will auto-assign random ports, which works perfectly
fine for client-only setups. If you need to accept inbound
connections, specify the listen port explicitly with
listen-port=13232.
Once created, grab your public keys with
/interface/wireguard print and assign IP addresses
matching whatever ranges your server or VPN provider has given
you:
/ip/address add address=10.7.1.2/24 interface=wg0
/ip/address add address=10.20.0.2/24 interface=wg1
Split Tunneling
This is where things get quite nice. The
allowed-address parameter on peers controls routing
through longest prefix match, which is a fancy way of saying
“most specific routes wins”.
For my main VPN tunnel (wg0), I set
allowed-address=0.0.0.0/0 to catch all traffic by
default:
/interface/wireguard/peers add \
interface=wg0 \
public-key="SERVER1_PUBLIC_KEY" \
endpoint-address=server1.example.com \
endpoint-port=51820 \
allowed-address=0.0.0.0/0 \
persistent-keepalive=25s
For my site-to-site mesh (wg1), I only specify the actual subnet I want to reach:
/interface/wireguard/peers add \
interface=wg1 \
public-key="SERVER2_PUBLIC_KEY" \
endpoint-address=server2.example.com \
endpoint-port=51820 \
allowed-address=10.20.0.0/24 \
persistent-keepalive=25s
RouterOS automatically installs routes based on these settings. Traffic to 10.20.0.0/24 uses the /24 route through wg1 (because it’s more specific), whilst everything else follows the default route through wg0. No manual route configuration needed.
If you need to add more subnets to a peer later, just update
the allowed-address parameter with comma-separated
networks. You can verify everything’s working properly with
/ip/route/print where gateway-status~"wireguard" to
see which routes RouterOS has installed.
Firewall Rules
Traffic routing through WireGuard interfaces must traverse the forward chain. This bit me initially because I’d forgotten to add accept rules before my drop-all rules. You need to allow both in-interface and out-interface for each WireGuard interface in the forward chain:
/ip/firewall/filter add chain=forward in-interface=wg0 action=accept comment="Allow WG0 inbound"
/ip/firewall/filter add chain=forward in-interface=wg1 action=accept comment="Allow WG1 inbound"
/ip/firewall/filter add chain=forward out-interface=wg0 action=accept comment="Allow WG0 outbound"
/ip/firewall/filter add chain=forward out-interface=wg1 action=accept comment="Allow WG1 outbound"
Critical point: These must go before any drop-all rules in your firewall. Otherwise nothing works and you’ll spend an hour debugging like I did.
If you’re accepting inbound connections, you’ll also need input chain rules to allow UDP on your listen ports. For NAT, add masquerade rules for each WireGuard interface in srcnat if local networks need to access resources through the tunnels:
/ip/firewall/nat add chain=srcnat out-interface=wg0 action=masquerade
/ip/firewall/nat add chain=srcnat out-interface=wg1 action=masquerade
Always-On VPN
Right, this is where it gets properly interesting. I wanted all my LAN traffic to route through my VPN provider by default, whilst keeping access to my mesh network and local management working. RouterOS 7.2+ routing tables make this possible without breaking everything.
First challenge: finding your VPN provider’s internal gateway. Most providers assign you a WireGuard IP but don’t always make the gateway obvious. Check their documentation for “internal gateway” or “SOCKS5 proxy address” - often they’re the same thing. Some providers list the gateway in their API documentation or configuration examples.
For this setup, you’ll need to reconfigure wg0 with your provider’s specific addressing. Many VPN providers use /32 addressing with an explicit gateway:
/ip/address/add interface=wg0 address=10.8.0.2/32 network=10.8.1.1
Replace with your provider’s assigned IP and gateway. Make
sure your wg0 peer is configured with the provider’s server
details and has allowed-address=0.0.0.0/0 for the
default route.
The approach is to create a dedicated routing table for VPN traffic with its own default route:
/routing/table/add name=vpn fib
/ip/route/add gateway=10.8.1.1 dst-address=0.0.0.0/0 routing-table=vpn
/routing/rule/add action=lookup-only-in-table table=vpn routing-mark=vpn
Replace 10.8.1.1 with your provider’s internal
gateway. Traffic marked with routing-mark=vpn uses
only this table, whilst unmarked traffic uses the main routing
table. Clever bit of isolation, that.
Here’s the critical part: without exclusions, you’ll break router management and mesh traffic. You need to create an address list for subnets that should NOT route through the VPN - your local LAN, the wg1 destinations, any remote LANs accessible through wg1, etc:
/ip/firewall/address-list/add address=192.168.1.0/24 list=noVPN comment="Local LAN"
/ip/firewall/address-list/add address=10.20.0.0/24 list=noVPN comment="wg1 destination"
/ip/firewall/address-list/add address=172.16.50.0/24 list=noVPN comment="wg1 remote LAN"
Then use a mangle rule to mark traffic for VPN routing:
/ip/firewall/mangle/add chain=prerouting in-interface=bridge \
dst-address-list=!noVPN \
action=mark-routing new-routing-mark=vpn connection-nat-state=!dstnat passthrough=no
Replace bridge with whatever your LAN interface
is called. This rule says “traffic from LAN to destinations NOT
in the noVPN list gets marked for VPN”. Marked traffic uses the
VPN routing table and exits via wg0. Excluded traffic uses the
main routing table and exits via WAN or wg1.
Why does this matter? Because wg1 routes exist in the MAIN
routing table (installed by allowed-address),
whilst the VPN routing table only has the default route through
wg0. Without exclusions, wg1 traffic would incorrectly try to
route through wg0, which would be properly confusing.
You’ll also need masquerade rules for both WAN and VPN interfaces, firewall rules to block unsolicited inbound from the VPN, and if you’re using your provider’s DNS servers, mangle rules to ensure DNS queries also route through the VPN.
CGNAT Client Relay
This last bit is for a fairly specific use case, but it’s quite important. I wanted one mobile device (behind CGNAT) to be able to use another device (also behind CGNAT) as an exit node when needed. The router acts as a relay between them.
The topology is: Client B (mobile device) connects to router, router forwards to Client A (exit node), Client A performs the actual NAT to internet. Both clients are behind CGNAT, but the router has a public IP.
On the router, you create a mesh interface with both clients as peers:
/interface/wireguard add name=wg-mesh listen-port=51820
/ip/address add address=10.99.0.1/24 interface=wg-mesh
/interface/wireguard/peers add \
interface=wg-mesh \
public-key="CLIENT_A_PUBLIC_KEY" \
allowed-address=10.99.0.2/32 \
persistent-keepalive=25s
/interface/wireguard/peers add \
interface=wg-mesh \
public-key="CLIENT_B_PUBLIC_KEY" \
allowed-address=10.99.0.3/32 \
persistent-keepalive=25s
/ip/firewall/filter add chain=forward in-interface=wg-mesh out-interface=wg-mesh \
action=accept comment="Allow WG mesh relay"
On Client A (the exit node), enable IP forwarding and add an iptables masquerade rule:
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
For most home setups, masquerading all traffic exiting your
internet interface works fine. If you want more control, you can
masquerade only the WireGuard subnet with
-s 10.99.0.0/24, but that’s only really necessary
if you’ve got a public IP and want to preserve source addresses
for non-VPN traffic.
On Client B (the mobile device), I’ve got two WireGuard profiles. Normal mode routes only the mesh subnet and Client A’s LAN through the tunnel:
[Interface]
Address = 10.99.0.3/24
PrivateKey = CLIENT_B_PRIVATE_KEY
[Peer]
PublicKey = ROUTER_PUBLIC_KEY
Endpoint = router.example.com:51820
AllowedIPs = 10.99.0.0/24, 192.168.10.0/24
PersistentKeepalive = 25
Exit node mode routes everything:
[Interface]
Address = 10.99.0.3/24
PrivateKey = CLIENT_B_PRIVATE_KEY
[Peer]
PublicKey = ROUTER_PUBLIC_KEY
Endpoint = router.example.com:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
I just toggle between profiles as needed. Quite neat for getting around public WiFi restrictions or just having a different exit point when needed.
Troubleshooting
A few common issues I’ve run into:
If handshakes aren’t occurring, check your firewall allows
UDP on the listen-port, verify endpoint addresses are correct,
and make sure persistent-keepalive is set
(absolutely essential for CGNAT scenarios).
If traffic isn’t routing through the VPN properly, verify
mangle rules are before any drop rules, check that routing marks
are being applied with
/ip/firewall/mangle print stats, and confirm your
VPN routing table exists and has the default route.
If you can’t reach your router from the LAN after enabling always-on VPN, make sure your LAN subnet is in the noVPN address-list. I forgot this initially and locked myself out, which was mildly embarrassing.
If your wg1 mesh stops working when VPN is active, add the wg1 destination subnets to the noVPN address-list. Remember, wg1 routes live in the main table, not the VPN table.
Some useful verification commands:
# Check WireGuard status and recent handshakes
/interface/wireguard/peers print
# Verify routes are installed correctly
/ip/route/print where gateway-status~"wireguard"
# Check if mangle rules are marking traffic
/ip/firewall/mangle print stats
# Verify routing tables
/routing/table/print
/routing/rule/print
Thoughts
WireGuard on MikroTik is incredibly flexible once you understand the routing table and mangle system. The key insight is that routing tables give you proper isolation - you can have completely separate routing logic for different traffic types without them interfering with each other.
The mangle-based routing approach has minimal overhead for typical home or small business use. I’m not pushing multi-gigabit throughput, so the flexibility far outweighs any marginal performance impact. If you need maximum throughput with hardware offload, you might want to keep things simpler, but for most scenarios this approach works brilliantly.
I’ve been running this setup for a few months now and it’s been rock solid. All my general internet traffic goes through my VPN provider whilst my mesh network and local access work perfectly. The CGNAT relay means I can use one device as an exit node from another when needed, which has been unsurprisingly useful.
If you’re setting up something similar, take it step by step. Get one tunnel working first, then add the second, then layer on the routing table complexity. Trying to do everything at once is a recipe for confusion (I learned this the hard way).
Hopefully this has been useful, or at the very least interesting. If you’ve got questions or run into issues, the MikroTik forums are quite helpful, though admittedly the documentation can be a bit sparse for advanced routing scenarios like this.