Tech Enabled Dad

Site to Site VPN with Wireguard

I recently moved and because of which my networking requirements got more complicated. We went from a multigenerational home into two single family homes. With this I now needed to not only manage two networks, but also share services between the two networks as well.

Now the point of this post, which is I wrote a script that got a site to site wireguard vpn up and running between the two houses. With testing we've been able to easily stream video, audio, books, etc. across the link with no issues. Not the biggest of deals I suppose but networking has never been my strongest skill so I was pretty happy when I got this working :D

################################################################################
# MikroTik RouterOS 7.x generic site-to-site WireGuard
#
# Purpose:
#   Build a reusable router-to-router WireGuard tunnel between two private LANs.
#
# Usage:
#   1. Set the site variable to "primary" or "secondary".
#   2. Adjust the LANs, tunnel IPs, port, and interface name if needed.
#   3. Run the script once on each router to create the interface, tunnel IP,
#      static route, firewall rules, and to print the local public key/DDNS.
#   4. Copy the printed DDNS names and public keys into the placeholders below.
#   5. Run the script again on each router to create or update the peer.
#
# Notes:
#   - This script enables RouterOS Cloud DDNS so dynamic WAN IPs can be used as
#     WireGuard endpoints.
#   - It is safe to rerun; objects are matched by name/comment before adding.
################################################################################

# Change this before importing on each router.
:local site "primary"

# Fill these in after the first pass.
:local primaryEndpoint "REPLACE_WITH_PRIMARY_DDNS"
:local secondaryEndpoint "REPLACE_WITH_SECONDARY_DDNS"
:local primaryPeerPublicKey "REPLACE_WITH_PRIMARY_PUBLIC_KEY"
:local secondaryPeerPublicKey "REPLACE_WITH_SECONDARY_PUBLIC_KEY"

:local wgName "WG-S2S-PRIMARY-SECONDARY"
:local wgComment "Generic site-to-site WireGuard tunnel"
:local wgListenPort 51820
:local wgMtu 1420

# Example LANs on each side of the tunnel.
:local primaryLan "192.168.10.0/24"
:local secondaryLan "192.168.20.0/24"

# Point-to-point tunnel addressing.
:local primaryTunnel "172.31.255.1/30"
:local secondaryTunnel "172.31.255.2/30"
:local tunnelPeerPrimary "172.31.255.1/32"
:local tunnelPeerSecondary "172.31.255.2/32"

:if (($site != "primary") && ($site != "secondary")) do={
    :error "Set :local site to primary or secondary before importing this script"
}

:local localLan ""
:local remoteLan ""
:local localTunnel ""
:local remoteAllowed ""
:local remoteEndpoint ""
:local remotePublicKey ""
:local routeComment ""
:local forwardRuleComment ""

:if ($site = "primary") do={
    :set localLan $primaryLan
    :set remoteLan $secondaryLan
    :set localTunnel $primaryTunnel
    :set remoteAllowed ($tunnelPeerSecondary . "," . $secondaryLan)
    :set remoteEndpoint $secondaryEndpoint
    :set remotePublicKey $secondaryPeerPublicKey
    :set routeComment "S2S WireGuard: route to secondary LAN"
    :set forwardRuleComment "S2S WireGuard: primary LAN to secondary LAN"
} else={
    :set localLan $secondaryLan
    :set remoteLan $primaryLan
    :set localTunnel $secondaryTunnel
    :set remoteAllowed ($tunnelPeerPrimary . "," . $primaryLan)
    :set remoteEndpoint $primaryEndpoint
    :set remotePublicKey $primaryPeerPublicKey
    :set routeComment "S2S WireGuard: route to primary LAN"
    :set forwardRuleComment "S2S WireGuard: secondary LAN to primary LAN"
}

:put ("Applying generic site-to-site WireGuard config for " . $site)

# RouterOS Cloud gives each router a stable DDNS name for its dynamic WAN IP.
/ip cloud set ddns-enabled=yes update-time=yes

# Create or update the WireGuard interface.
:local wgId [/interface wireguard find where name=$wgName]
:if ([:len $wgId] = 0) do={
    /interface wireguard add name=$wgName listen-port=$wgListenPort mtu=$wgMtu comment=$wgComment
    :set wgId [/interface wireguard find where name=$wgName]
} else={
    /interface wireguard set $wgId listen-port=$wgListenPort mtu=$wgMtu comment=$wgComment disabled=no
}

# Add the local tunnel IP if it does not already exist.
:if ([:len [/ip address find where interface=$wgName address=$localTunnel]] = 0) do={
    /ip address add address=$localTunnel interface=$wgName comment=$wgComment
}

# Add or update the static route that points the remote LAN at the WireGuard
# interface.
:local routeId [/ip route find where comment=$routeComment]
:if ([:len $routeId] = 0) do={
    :set routeId [/ip route find where dst-address=$remoteLan gateway=$wgName]
}
:if ([:len $routeId] = 0) do={
    /ip route add dst-address=$remoteLan gateway=$wgName comment=$routeComment
} else={
    /ip route set $routeId dst-address=$remoteLan gateway=$wgName comment=$routeComment disabled=no
}

# Allow the WireGuard listener in from the WAN before the default input drop.
:local inputRuleComment ("S2S WireGuard: allow UDP " . $wgListenPort)
:if ([:len [/ip firewall filter find where chain=input comment=$inputRuleComment]] = 0) do={
    :local inputDropRule [/ip firewall filter find where chain=input comment="drop all else"]
    :if ([:len $inputDropRule] = 0) do={
        /ip firewall filter add chain=input action=accept in-interface-list=WAN protocol=udp dst-port=$wgListenPort comment=$inputRuleComment
    } else={
        /ip firewall filter add chain=input action=accept in-interface-list=WAN protocol=udp dst-port=$wgListenPort comment=$inputRuleComment place-before=$inputDropRule
    }
}

# Allow tunnel traffic before FastTrack so site-to-site flows stay on the
# normal forwarding path.
:local forwardPlaceRule [/ip firewall filter find where chain=forward comment="defconf: fasttrack"]
:if ([:len $forwardPlaceRule] = 0) do={
    :set forwardPlaceRule [/ip firewall filter find where chain=forward comment="drop all else"]
}

:if ([:len [/ip firewall filter find where chain=forward comment=$forwardRuleComment]] = 0) do={
    :if ([:len $forwardPlaceRule] = 0) do={
        /ip firewall filter add chain=forward action=accept src-address=$localLan dst-address=$remoteLan comment=$forwardRuleComment
    } else={
        /ip firewall filter add chain=forward action=accept src-address=$localLan dst-address=$remoteLan comment=$forwardRuleComment place-before=$forwardPlaceRule
    }
}

# Add the complementary rule as well so rerunning the same script on either
# router produces a complete and symmetric rule set.
:local reverseForwardRuleComment ""
:if ($site = "primary") do={
    :set reverseForwardRuleComment "S2S WireGuard: secondary LAN to primary LAN"
} else={
    :set reverseForwardRuleComment "S2S WireGuard: primary LAN to secondary LAN"
}

:if ([:len [/ip firewall filter find where chain=forward comment=$reverseForwardRuleComment]] = 0) do={
    :if ([:len $forwardPlaceRule] = 0) do={
        /ip firewall filter add chain=forward action=accept src-address=$remoteLan dst-address=$localLan comment=$reverseForwardRuleComment
    } else={
        /ip firewall filter add chain=forward action=accept src-address=$remoteLan dst-address=$localLan comment=$reverseForwardRuleComment place-before=$forwardPlaceRule
    }
}

# Print the local values needed to finish the opposite side.
:put ("Local public key: " . [/interface wireguard get $wgId public-key])
:put ("Local DDNS name: " . [/ip cloud get dns-name])

# Only create or update the peer after the remote details are filled in.
:if (($remoteEndpoint = "REPLACE_WITH_PRIMARY_DDNS") || ($remoteEndpoint = "REPLACE_WITH_SECONDARY_DDNS") || ($remotePublicKey = "REPLACE_WITH_PRIMARY_PUBLIC_KEY") || ($remotePublicKey = "REPLACE_WITH_SECONDARY_PUBLIC_KEY")) do={
    :put "Remote endpoint/public key placeholders are still set."
    :put "Update the variables at the top of this script and rerun it to add the peer."
} else={
    :local peerComment ("S2S WireGuard peer for " . $site)
    :local peerId [/interface wireguard peers find where interface=$wgName comment=$peerComment]
    :if ([:len $peerId] = 0) do={
        /interface wireguard peers add interface=$wgName comment=$peerComment public-key=$remotePublicKey endpoint-address=$remoteEndpoint endpoint-port=$wgListenPort allowed-address=$remoteAllowed persistent-keepalive=25s
    } else={
        /interface wireguard peers set $peerId public-key=$remotePublicKey endpoint-address=$remoteEndpoint endpoint-port=$wgListenPort allowed-address=$remoteAllowed persistent-keepalive=25s disabled=no
    }
    :put ("Peer configured for remote LAN " . $remoteLan)
}