Octet

Join two LANs with a WireGuard site-to-site tunnel on Linux

LinuxWorking

Tasks

  1. Turn on IPv4 forwarding on both gateways.
  2. Generate a key pair on each gateway and exchange the public keys.
  3. Write wg0.conf on each gateway with the right AllowedIPs for the peer's LAN.
  4. Open UDP 51820 and allow forwarding between wg0 and the LAN.
  5. Bring the tunnel up at boot and verify the handshake, routes and LAN-to-LAN traffic.

Solution

  1. 1Enable IPv4 forwarding (both gateways)GW1
    GW1 on Linux
    echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-forwarding.conf
    sysctl --system

    Do the same on GW2. Without forwarding, the gateways answer pings to themselves but never route LAN traffic.

  2. 2Generate a key pair (both gateways)GW1
    GW1 on Linux
    umask 077
    wg genkey | tee /etc/wireguard/wg0.key | wg pubkey > /etc/wireguard/wg0.pub
    cat /etc/wireguard/wg0.pub

    The private key never leaves the gateway. Only the public key goes to the peer, and it is safe to send by any channel.

  3. 3Tunnel configuration on GW1GW1
    GW1 on Linux
    # /etc/wireguard/wg0.conf
    [Interface]
    Address = 172.16.99.1/30
    ListenPort = 51820
    PrivateKey = <contents of /etc/wireguard/wg0.key on GW1>
    
    [Peer]
    # GW2
    PublicKey = AdhgM1fPr4DFGooiULo7imk0kdnXVykxN6upQFgdKDk=
    Endpoint = 203.0.113.20:51820
    AllowedIPs = 172.16.99.2/32, 10.2.0.0/24
    PersistentKeepalive = 25

    wg-quick adds a route for each AllowedIPs prefix that is not already covered, so 10.2.0.0/24 is routed into wg0 automatically.

  4. 4Tunnel configuration on GW2GW2
    GW2 on Linux
    # /etc/wireguard/wg0.conf
    [Interface]
    Address = 172.16.99.2/30
    ListenPort = 51820
    PrivateKey = <contents of /etc/wireguard/wg0.key on GW2>
    
    [Peer]
    # GW1
    PublicKey = XPUokPLW1NGGoMksYc6MrU2IpIqQazgHL9uww/NCJzk=
    Endpoint = 198.51.100.10:51820
    AllowedIPs = 172.16.99.1/32, 10.1.0.0/24
    PersistentKeepalive = 25

    AllowedIPs here must include GW1's LAN, or packets from 10.1.0.0/24 are decrypted and then dropped because their source is not allowed for that peer.

  5. 5Firewall rules (both gateways)GW1
    GW1 on Linux
    nft add rule inet filter input udp dport 51820 accept
    nft add rule inet filter forward iifname "wg0" oifname "eth1" accept
    nft add rule inet filter forward iifname "eth1" oifname "wg0" accept

    These rules are not persistent. Add them to the nftables configuration file your distribution loads at boot.

  6. 6Bring the tunnel up now and at boot (both gateways)GW1
    GW1 on Linux
    systemctl enable --now wg-quick@wg0

Verify

GW1 output
GW1# wg show wg0
interface: wg0
  public key: XPUokPLW1NGGoMksYc6MrU2IpIqQazgHL9uww/NCJzk=
  private key: (hidden)
  listening port: 51820

peer: AdhgM1fPr4DFGooiULo7imk0kdnXVykxN6upQFgdKDk=
  endpoint: 203.0.113.20:51820
  allowed ips: 172.16.99.2/32, 10.2.0.0/24
  latest handshake: 38 seconds ago
  transfer: 18.42 KiB received, 21.07 KiB sent
  persistent keepalive: every 25 seconds

A recent handshake and bytes in both directions. No "latest handshake" line at all means the handshake never completed, so check keys, endpoint and UDP 51820.

GW1 output
GW1# ip route | grep wg0
10.2.0.0/24 dev wg0 scope link
172.16.99.0/30 dev wg0 proto kernel scope link src 172.16.99.1

The remote LAN is routed into wg0. The /32 for GW2's tunnel address needed no route because the connected /30 already covers it.

GW1 output
GW1# ping -c 3 -I 10.1.0.1 10.2.0.10
PING 10.2.0.10 (10.2.0.10) from 10.1.0.1 : 56(84) bytes of data.
64 bytes from 10.2.0.10: icmp_seq=1 ttl=63 time=24.1 ms
64 bytes from 10.2.0.10: icmp_seq=2 ttl=63 time=23.8 ms
64 bytes from 10.2.0.10: icmp_seq=3 ttl=63 time=23.9 ms

--- 10.2.0.10 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 23.812/23.933/24.104/0.124 ms

Sourcing from the LAN address tests the real LAN-to-LAN policy, including GW2's AllowedIPs for 10.1.0.0/24. TTL 63 is SRV2's 64 minus one hop through GW2.

Mistakes people make

  • Leaving the peer's LAN out of AllowedIPs. The handshake works, but LAN traffic is either not routed into wg0 or dropped after decryption.
  • Forgetting net.ipv4.ip_forward. The tunnel is up, the gateways can ping each other, and nothing behind them works.
  • Swapping keys, for example putting a gateway's own public key in its [Peer] section. The handshake never completes, and WireGuard logs nothing about it by default.
  • Listing the same prefix in two peers on one interface. The prefix silently moves to the peer configured last.
  • Hosts on the LAN whose default gateway is not the WireGuard gateway. They need a route for the remote LAN pointing at it.

Say it in the interview

"Each gateway gets a key pair, and each side lists the other as a peer with its public key, endpoint and AllowedIPs. AllowedIPs is the important line: it routes the remote LAN into the tunnel and filters the source of what comes out of it, so each side lists the other's tunnel address and LAN. Then forwarding, a firewall hole for UDP 51820 and forward rules between wg0 and the LAN. I verify with wg show for a recent handshake, the route into wg0, and a ping sourced from the LAN."

Book 8.5 Example Systems