WireGuard VPN tunnels
File: src/topics/wireguard.j. Paths: /interface/wireguard (WIREGUARD_PATH), /interface/wireguard/peers (WIREGUARD_PEER_PATH). Reference: cheatsheet.
Needs RouterOS v7. wireguardInterfaces and wireguardPeers return an empty list on any read error: on v6, but also on a dropped connection or a permission error, so an empty list does not prove there are no tunnels.
The local end is a WireGuard interface (keypair, UDP listen port); each remote party is a peer on it.
- The router generates the private key; this deck neither sets nor reads it. You handle public keys only: 44 base64 characters ending in
=, checked locally. - allowed-address is a filter and a route: traffic from a peer is accepted only for these networks, and traffic to them goes into the tunnel. Every entry needs a prefix; a single device is
/32, and a bare IP is rejected with that hint. - A live tunnel handshakes about every 2 minutes;
lastHandshakeis the liveness signal.
Road-warrior server
# interface, VPN address, firewall opening (idempotent)
mt.setupWireguardServer($c, "wgvpn", 13231, "10.100.0.1/24");
# the router's public key, for the laptop's WireGuard config
io.printf("server public key: %s\n", mt.wireguardPublicKey($c, "wgvpn"));
# the laptop's public key comes from its WireGuard app
mt.addWireguardPeer($c, "wgvpn",
"hIe5R2FTPjvrIrVXfy41chvUJ4CyOTdBEI3pQGea0R0=",
"10.100.0.2/32", "laptop");The laptop's config: endpoint = the router's public IP or DNS name, port 13231, address 10.100.0.2/32, allowed IPs at least 10.100.0.0/24. To reach the LAN as well, add 192.168.88.0/24 to the allowed IPs on the laptop and permit VPN to LAN in the forward chain.
Site-to-site, this router dials out
mt.addWireguard($c, "wghome", 13231);
io.printf("our key (for the far end): %s\n", mt.wireguardPublicKey($c, "wghome"));
mt.connectWireguard($c, "wghome",
"<the far end's public key>", "vpn.example.org", 13231,
"10.100.0.0/24,192.168.10.0/24", # what lives behind the far end
"to headquarters");
mt.addIpAddress($c, "10.100.0.3/24", "wghome");Monitoring
def peers as list of mt.WireguardPeer init mt.wireguardPeers($c);
for (def p in $peers) {
io.printf("peer %s: handshake %s, rx %s tx %s\n",
$p.comment, $p.lastHandshake, $p.rx, $p.tx);
}An empty lastHandshake, or one older than 2-3 minutes, means the tunnel is down: wrong key, wrong endpoint, or UDP blocked. WireGuard reports a wrong key nowhere else.
Pitfalls
- Overlapping allowed-addresses between peers on one interface do not work; the last one wins.
- The input chain must accept UDP on the listen port.
setupWireguardServeradds that rule (comment"wireguard: <name>");addWireguardalone does not. - Behind NAT only the dialing side needs keepalive;
connectWireguardsets 25 seconds. - Traffic between VPN and LAN passes the
forwardchain; permit it if peers should reach LAN devices (firewall.md). removeWireguardremoves only the first IP address on the interface.