Skip to main content
RunBook Academy

← All labs in VyOS

Lab · advanced · ~100 min

Lab: WireGuard Site-to-Site

B · Nested virtualisationC · Simulation

Objectives

  • Build a two-site WireGuard lab from a stated starting state, including the bridge and the VM definitions
  • Generate a key pair on each router separately and exchange only the public halves
  • Name every prefix twice — once in the routing table and once in allowed-ips — and prove why both are needed
  • Read a tunnel from the handshake and the two transfer counters instead of from a ping
  • Separate three failures that all look like silence: a blocked underlay, a far-end allowed-ips gap, and an MTU black hole
  • Measure the usable tunnel MTU rather than assuming the default, and clamp TCP beneath it

Prerequisites

Objective

By the end of this lab one WireGuard tunnel will carry traffic between two sites, and you will be able to state which of four separate things is true at any moment: the underlay reaches the far router, the handshake completed, packets are being encrypted and sent, and packets are being received and accepted. Those four fail independently, and WireGuard’s defining operational property is that it reports none of them by complaining. It reports them by staying silent.

That silence is the reason this lab spends more time on evidence than on configuration. The configuration is about fifteen lines per router. The skill is knowing which two numbers to read, and in which order, when it does not work.

Architecture

Two VyOS routers, each standing in for a site edge. Each carries a dummy interface holding the prefix its site owns, so neither needs a host behind it to have something worth routing.

   SITE A                                          SITE B
   dum0 192.168.10.1/24                            dum0 192.168.20.1/24
        |                                               |
   +----+---------------------+     +-------------------+----+
   |        site-a            |     |         site-b         |
   |  eth1 198.51.100.10/24   +-----+  eth1 198.51.100.20/24 |
   |  wg0  10.10.10.1/30      |     |  wg0  10.10.10.2/30    |
   +--------------------------+     +------------------------+
        |            vmbr95 — the "internet" segment,             |
        |            198.51.100.0/24, no uplink                   |
        |                                                        |
   +----+--------------------------------------------------------+
   |            vmbr0 — your lab management LAN                   |
   +--------------------------------------------------------------+

Three address families doing three jobs, and keeping them apart is what makes the failures legible. 198.51.100.0/24 is the underlay: the network the encrypted UDP crosses, and the only one that exists before the tunnel does. 10.10.10.0/30 is the tunnel transport, a point-to-point link that exists only inside the encryption. 192.168.10.0/24 and 192.168.20.0/24 are the payload prefixes — the traffic the sites actually care about.

vmbr95 is an isolated bridge with no physical uplink. vmbr0 carries management only and nothing in this lab touches it, which matters in Task 5 where you install a default-deny input policy.

Requirements

  • A hypervisor able to run two VMs with two NICs each — one on your management LAN and one on an isolated bridge. The commands are written for Proxmox VE; any KVM/libvirt host works.
  • 1 GB RAM and 8 GB disk per router — 2 GB and 16 GB in total.
  • The VyOS 1.5 LTS ISO uploaded to hypervisor storage. WireGuard is in the base image; there is nothing to install.
  • Console access to both VMs, and this one is not optional. Task 5 sets firewall ipv4 input filter default-action drop, which governs traffic addressed to the router itself — including your SSH session. The task adds a management permit before the default action for exactly that reason, and if you commit them in the wrong order you will lose the session and need the console to recover.
  • Roughly 100 minutes, of which the VM build is about 25.

Scenario

Two offices need a private path between them. There is no MPLS, no existing IPsec, and no appetite for a device in the middle: both sites have a VyOS router with a public address and that is the whole inventory. Site A owns 192.168.10.0/24, Site B owns 192.168.20.0/24, and neither prefix may appear on the public network.

Nothing is configured yet. You will build the starting state.

Tasks

Task 1: Build the starting state

One isolated bridge. On the Proxmox host, add a Linux bridge with no ports — no ports is what makes it isolated. This is config text for /etc/network/interfaces, not a command:

auto vmbr95
iface vmbr95 inet manual
    bridge-ports none
    bridge-stp off
    bridge-fd 0

Apply with ifreload -a, or reboot the node if your host has no ifupdown2. Then create the two VMs:

# Run on the Proxmox host.
# Substitute your own values before running. The ISO volume ID must match
# exactly what `pvesm list local` prints for your upload.
ISO=local:iso/vyos-1.5-lts-amd64.iso
STORE=local-lvm

for VMID in 280 281; do
  qm create "$VMID" --memory 1024 --cores 1 \
    --net0 "virtio,bridge=vmbr0,firewall=0" \
    --net1 "virtio,bridge=vmbr95,firewall=0" \
    --scsihw virtio-scsi-single --scsi0 "$STORE:8" \
    --ide2 "$ISO,media=cdrom" --boot order=ide2 --ostype l26
done

qm set 280 --name site-a
qm set 281 --name site-b

qm start 280
qm start 281

firewall=0 on the NICs matters more than usual here. WireGuard is UDP on port 51820 and nothing else; a host-side filter that drops it produces a tunnel that never handshakes and logs nothing at either end, which is the single hardest WireGuard symptom to attribute.

Install on each VM from the console — log in with the live-boot credentials, run install image, accept the defaults — then move the boot order back to disk and detach the ISO:

# Run on the Proxmox host, after `install image` completes on both VMs.
for VMID in 280 281; do
  qm set "$VMID" --boot order=scsi0
  qm set "$VMID" --delete ide2
done

Task 2: Baseline, and prove the underlay before you encrypt anything

On site-a:

configure
set system host-name site-a
set interfaces ethernet eth0 address dhcp
set interfaces ethernet eth0 description "management - not part of the lab"
set interfaces ethernet eth1 address 198.51.100.10/24
set interfaces ethernet eth1 description "underlay - the public side"
set interfaces dummy dum0 address 192.168.10.1/24
set interfaces dummy dum0 description "site A payload prefix"
set service ssh
commit
save

On site-b, the mirror image:

configure
set system host-name site-b
set interfaces ethernet eth0 address dhcp
set interfaces ethernet eth0 description "management - not part of the lab"
set interfaces ethernet eth1 address 198.51.100.20/24
set interfaces ethernet eth1 description "underlay - the public side"
set interfaces dummy dum0 address 192.168.20.1/24
set interfaces dummy dum0 description "site B payload prefix"
set service ssh
commit
save

Save a named baseline on both; Cleanup restores from it:

save /config/pre-lab.boot

Prove the underlay works before adding a tunnel to it:

Read-only / Safesite-a
$ ping 198.51.100.20 count 3
PING 198.51.100.20 (198.51.100.20) 56(84) bytes of data.
64 bytes from 198.51.100.20: icmp_seq=1 ttl=64 time=0.482 ms
64 bytes from 198.51.100.20: icmp_seq=2 ttl=64 time=0.401 ms
64 bytes from 198.51.100.20: icmp_seq=3 ttl=64 time=0.398 ms

--- 198.51.100.20 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2039ms

Illustrative output

This is the one moment in the lab where ping between the routers proves something. Once the tunnel exists, this ping will keep succeeding through every failure you are about to create — the underlay and the tunnel are independent, and confusing them is how an hour goes missing.

Task 3: Keys, generated in two places

Generate a key pair on each router, separately. Enter configuration mode on site-a with configure, then run the generator from inside the session — the install form only works there, because that is where a set can be staged:

Configuration changesite-a
$ run generate pki wireguard key-pair install interface wg0
1 value(s) installed. Use "compare" to see the pending changes, and "commit" to apply.
Corresponding public-key to use on peer system is: 'rSuuvkAuSoyqvogoA5OJjYBbJKKaYS13pWs4K69PTOE='

Illustrative output

The private half is written straight into the configuration and never passes through your clipboard, your scrollback or your ticket. Run the same command outside configuration mode and it installs nothing; it prints the set line for you to paste instead, which is a message rather than an error.

Do the same on site-b. Record both public keys in your journal against the router they belong to — this is the key inventory, and it is a deliverable because WireGuard gives you nothing else. There is no certificate, no subject name, no chain to walk: a peer’s identity is its 44 characters of base64, and wg will only ever show you the key.

Now the interfaces. On site-a, still inside the configuration session that staged the private key:

set interfaces wireguard wg0 address 10.10.10.1/30
set interfaces wireguard wg0 description "site-to-site to site-b"
set interfaces wireguard wg0 port 51820

set interfaces wireguard wg0 peer SITE-B public-key 'cVn4T2sM8xQ6yB1hJ0dR7kL3pW9zA5eG2uY8iO4nX1c='
set interfaces wireguard wg0 peer SITE-B address 198.51.100.20
set interfaces wireguard wg0 peer SITE-B port 51820
set interfaces wireguard wg0 peer SITE-B allowed-ips 10.10.10.0/30
set interfaces wireguard wg0 peer SITE-B allowed-ips 192.168.20.0/24

set protocols static route 192.168.20.0/24 interface wg0
commit
save

On site-b, the mirror image — note that each router holds the other router’s public key, which is the mistake worth being deliberate about:

set interfaces wireguard wg0 address 10.10.10.2/30
set interfaces wireguard wg0 description "site-to-site to site-a"
set interfaces wireguard wg0 port 51820

set interfaces wireguard wg0 peer SITE-A public-key 'rSuuvkAuSoyqvogoA5OJjYBbJKKaYS13pWs4K69PTOE='
set interfaces wireguard wg0 peer SITE-A address 198.51.100.10
set interfaces wireguard wg0 peer SITE-A port 51820
set interfaces wireguard wg0 peer SITE-A allowed-ips 10.10.10.0/30
set interfaces wireguard wg0 peer SITE-A allowed-ips 192.168.10.0/24

set protocols static route 192.168.10.0/24 interface wg0
commit
save

Substitute the public keys your own routers printed. The values above are the examples from this course’s lessons and belong to nothing.

Task 4: Prove the tunnel four times

Four independent claims, four different commands. Take all four; the failure tasks are about watching them come apart.

Claim one — the handshake completed and bytes have moved in both directions. This is the command to run first, always, and on both ends:

Read-only / Safesite-a
$ show interfaces wireguard wg0 summary
interface: wg0
public key: rSuuvkAuSoyqvogoA5OJjYBbJKKaYS13pWs4K69PTOE=
private key: (hidden)
listening port: 51820

peer: cVn4T2sM8xQ6yB1hJ0dR7kL3pW9zA5eG2uY8iO4nX1c=
endpoint: 198.51.100.20:51820
allowed ips: 10.10.10.0/30, 192.168.20.0/24
latest handshake: 41 seconds ago
transfer: 4.52 KiB received, 5.18 KiB sent

Illustrative output

Three things carry the answer. latest handshake measured in seconds or a couple of minutes means the keys match and the underlay works. Two non-zero transfer counters mean packets are crossing in both directions. The peer is identified by key, not by the name SITE-B you chose — that name exists in the configuration and nowhere else, so show configuration commands | match wireguard is the lookup that turns a key back into a site.

Absence is also information. wg prints the latest handshake line only once there has been one, and the transfer line only once a byte has moved. A peer that has never come up appears as three lines and no “never” — the missing lines are the report.

Claim two — the encrypted packets are on the underlay.

Read-only / Safesite-b
$ sudo tcpdump -ni eth1 'udp port 51820' -c 4
14:06:11.884210 IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 128
14:06:11.884702 IP 198.51.100.20.51820 > 198.51.100.10.51820: UDP, length 32
14:06:12.885991 IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 128

Illustrative output

UDP both ways, and nothing legible. There is no protocol negotiation to watch and no session to inspect — an observer on the underlay learns the two endpoints, the timing and the sizes, and nothing else.

Claim three — the tunnel transport works.

Read-only / Safesite-a
$ ping 10.10.10.2 count 3
PING 10.10.10.2 (10.10.10.2) 56(84) bytes of data.
64 bytes from 10.10.10.2: icmp_seq=1 ttl=64 time=1.102 ms
64 bytes from 10.10.10.2: icmp_seq=2 ttl=64 time=0.921 ms
64 bytes from 10.10.10.2: icmp_seq=3 ttl=64 time=0.944 ms

--- 10.10.10.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2043ms

Illustrative output

Claim four — the payload prefixes route across it. This is the one that matters to a user, and it is a different claim from the one above, because it exercises the static route and the second entry in allowed-ips:

Read-only / Safesite-a
$ ping 192.168.20.1 interface dum0 count 3
PING 192.168.20.1 (192.168.20.1) from 192.168.10.1 dum0: 56(84) bytes of data.
64 bytes from 192.168.20.1: icmp_seq=1 ttl=64 time=1.208 ms
64 bytes from 192.168.20.1: icmp_seq=2 ttl=64 time=0.988 ms
64 bytes from 192.168.20.1: icmp_seq=3 ttl=64 time=1.011 ms

--- 192.168.20.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2041ms

Illustrative output

interface dum0 forces the source address to Site A’s payload prefix, which is what a real host behind the router would use. Without it the router sources from 10.10.10.1 and you have re-run claim three with a different destination.

Keep all four captures.

Task 5: Silence, cause one — the underlay is filtered

Give site-b the input policy a public-facing router should have. Order matters: the management permit is committed in the same operation as the default action, and if you commit the default action alone you will lose your session.

configure
set firewall ipv4 input filter rule 1 action accept
set firewall ipv4 input filter rule 1 description "management - keep this first"
set firewall ipv4 input filter rule 1 inbound-interface name eth0

set firewall ipv4 input filter rule 5 action accept
set firewall ipv4 input filter rule 5 description "established and related to the router"
set firewall ipv4 input filter rule 5 state established
set firewall ipv4 input filter rule 5 state related

set firewall ipv4 input filter default-action drop
set firewall ipv4 input filter default-log
commit-confirm 5

Type confirm once you have checked you still have a session. Note what is deliberately missing: nothing permits UDP 51820.

Wait a couple of minutes, then look at both routers.

Read-only / Safesite-a
$ show interfaces wireguard wg0 summary
interface: wg0
public key: rSuuvkAuSoyqvogoA5OJjYBbJKKaYS13pWs4K69PTOE=
private key: (hidden)
listening port: 51820

peer: cVn4T2sM8xQ6yB1hJ0dR7kL3pW9zA5eG2uY8iO4nX1c=
endpoint: 198.51.100.20:51820
allowed ips: 10.10.10.0/30, 192.168.20.0/24
latest handshake: 3 minutes, 12 seconds ago
transfer: 4.52 KiB received, 9.86 KiB sent

Illustrative output

The handshake is ageing and sent is climbing while received is not. site-a is initiating and getting nothing back. Confirm from the wire on the far side:

Read-only / Safesite-b
$ sudo tcpdump -ni eth1 'udp port 51820' -c 4
14:22:07.118442 IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148
14:22:12.121004 IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148
14:22:17.123551 IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148

Illustrative output

This is the pair that people mis-read. The packets arrive, and there is no reply, and the obvious conclusion is a key mismatch — a router that will not authenticate also arrives-and-does-not-reply. But tcpdump on an ingress interface sees the packet before the input chain does, so “arrived” here means “reached the NIC”, not “reached WireGuard”.

The counter settles it:

Read-only / Safesite-b
$ show firewall ipv4 input filter
Ruleset Information

ipv4 Firewall "input filter"

Rule     Action    Protocol    Packets    Bytes    Conditions
-------  --------  ----------  ---------  -------  ------------------------------
1        accept    all         318        41K      iifname eth0
5        accept    all         12         960      state established,related
default  drop                  27         3.9K

Illustrative output

The default-action counter is climbing at the rate of the initiations. A key mismatch would leave it flat, because the packets would have been accepted by the firewall and discarded by WireGuard, which counts nothing. That is the discriminator: the drop counter separates “refused entry” from “refused authentication”, and nothing in show interfaces wireguard can tell them apart.

Fix it with a rule scoped to the peers you actually have:

configure
set firewall group address-group WIREGUARD-PEERS address 198.51.100.10
set firewall ipv4 input filter rule 10 action accept
set firewall ipv4 input filter rule 10 description "WireGuard from known peers"
set firewall ipv4 input filter rule 10 protocol udp
set firewall ipv4 input filter rule 10 destination port 51820
set firewall ipv4 input filter rule 10 source group address-group WIREGUARD-PEERS
set firewall ipv4 input filter rule 10 inbound-interface name eth1
commit-confirm 5

Confirm, and watch show interfaces wireguard wg0 summary on site-a: within seconds the handshake age resets and received starts moving.

The tunnel is up and ping 192.168.20.1 interface dum0 still fails, which is the second thing this policy teaches. The input chain governs traffic addressed to this router, and that includes the decrypted inner packets destined for site-b’s own dum0 and wg0 addresses. They arrive on wg0 rather than on eth1, so no rule you have written so far describes them:

set firewall ipv4 input filter rule 20 action accept
set firewall ipv4 input filter rule 20 description "from the tunnel to this router"
set firewall ipv4 input filter rule 20 inbound-interface name wg0
commit-confirm 5

Two rules, two hooks, two completely different packets: rule 10 admits the encrypted UDP on the underlay, rule 20 admits what comes out of it. A policy that has one and not the other produces a tunnel that handshakes perfectly and carries nothing, which is Task 6’s symptom arriving from a different cause — and the counters tell them apart, because a firewall drop increments something and an allowed-ips discard does not.

Task 6: Silence, cause two — the far end will not claim your source

Restore the tunnel to health, then remove Site A’s prefix from site-b’s peer definition — the mistake that copying one peer block to make another produces, and the one that is hardest to see by reading:

configure
delete interfaces wireguard wg0 peer SITE-A allowed-ips 192.168.10.0/24
commit

Now re-run the two pings from site-a.

ping 10.10.10.2 still works: 10.10.10.0/30 is still in the list. ping 192.168.20.1 interface dum0 fails, and it fails silently — no ICMP unreachable, no error, just no replies. Look at the counters on site-a:

Read-only / Safesite-a
$ show interfaces wireguard wg0 summary
peer: cVn4T2sM8xQ6yB1hJ0dR7kL3pW9zA5eG2uY8iO4nX1c=
endpoint: 198.51.100.20:51820
allowed ips: 10.10.10.0/30, 192.168.20.0/24
latest handshake: 22 seconds ago
transfer: 1.12 KiB received, 8.44 KiB sent

Illustrative output

A recent handshake with sent climbing faster than received is the signature of a far-end problem, and it is the most useful single reading this command produces. The keys are fine — a handshake proves that. Your packets are leaving. Something at the other end is receiving them and declining to answer.

Confirm it by capturing the plaintext view at both ends, with the ping still running:

Read-only / Safesite-a
$ sudo tcpdump -ni wg0 -c 4
14:41:02.118442 IP 192.168.10.1 > 192.168.20.1: ICMP echo request, id 4102, seq 1, length 64
14:41:03.119901 IP 192.168.10.1 > 192.168.20.1: ICMP echo request, id 4102, seq 2, length 64

Illustrative output

Run the same capture on site-b’s wg0 and nothing appears. The packet was decrypted correctly and discarded before it reached the interface, and there is exactly one thing that does that: the source address was not inside the allowed-ips of the peer it arrived from.

Put it back:

configure
set interfaces wireguard wg0 peer SITE-A allowed-ips 192.168.10.0/24
commit
save

Task 7: Silence, cause three — an MTU nothing ever told the tunnel about

Small packets work and large transfers stall. This is the failure that survives every check you have run so far, because everything you have tested has been small.

Constrain the underlay, the way a provider link or another tunnel in the path would. On both routers:

configure
set interfaces ethernet eth1 mtu 1400
commit

Now ping 192.168.20.1 interface dum0 still succeeds, and so does show interfaces wireguard wg0 summary. Everything reports healthy. Then measure, rather than assume, by bisecting with the do-not-fragment flag:

Read-only / Safesite-a
$ ping 192.168.20.1 interface dum0 size 1312 do-not-fragment count 2
PING 192.168.20.1 (192.168.20.1) from 192.168.10.1 dum0: 1312(1340) bytes of data.
1320 bytes from 192.168.20.1: icmp_seq=1 ttl=64 time=1.884 ms
1320 bytes from 192.168.20.1: icmp_seq=2 ttl=64 time=1.702 ms

--- 192.168.20.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms

Illustrative output

Now a payload that does not fit. Run it with a capture of the underlay going at the same time, because what you are looking for is on the outside, not in the ping:

Read-only / Safesite-a
$ ping 192.168.20.1 interface dum0 size 1392 do-not-fragment count 2
Read-only / Safesite-a
$ sudo tcpdump -ni eth1 -v host 198.51.100.20 -c 6

Record which of two things you see, because both are defects and which one you get depends on your hypervisor and driver. Either the ping gets no replies at all — the oversized encrypted packet was discarded — or the verbose capture shows the outer packet arriving as IP fragments, which reassemble and answer. Fragments are easy to spot in -v output: one encrypted packet becomes more than one line, carrying flags [+] and a non-zero offset. Fragmentation looks like success and is not — it costs a reassembly buffer at the far end, and any middlebox on a real path that drops fragments turns it back into the first case without warning.

The do-not-fragment flag in the ping applies to the inner packet. It is the tunnel’s own encapsulation that has decided whether the outer packet may be fragmented, and that decision is not yours to make from here — which is the whole reason the interface MTU has to be right.

The arithmetic is worth doing by hand once. WireGuard adds 60 bytes to every packet over an IPv4 underlay: 20 of outer IP header, 8 of UDP, 16 of message header and 16 of authentication tag. The interface default MTU is 1420, which is 1500 minus 80 — the IPv6 figure, chosen so the default is safe on either underlay.

With eth1 at 1400, the largest inner packet that fits is 1400 minus 60, which is 1340. An ICMP echo carries 28 bytes of headers, so the largest payload that fits is 1312 — and wg0, still at its default 1420, will happily accept a 1392-byte payload and hand the underlay a 1480-byte encrypted packet that does not fit.

Set the interface to what you measured, and clamp TCP beneath it:

configure
set interfaces wireguard wg0 mtu 1340
set interfaces wireguard wg0 ip adjust-mss clamp-mss-to-pmtu
set interfaces wireguard wg0 ipv6 adjust-mss clamp-mss-to-pmtu
commit
save

Re-run both pings. The 1312-byte one still succeeds. The 1392-byte one now fails immediately and locally, with a message from your own kernel naming the MTU it refused to exceed, because wg0 knows the packet is too big instead of handing the problem to the underlay. That change — from silence, or from a fragment nobody counted, to an error at the sender — is the entire value of setting the MTU from a measurement.

Production notes — running this as a change

Build one direction at a time and let it be asymmetric for a while. Configure site-b fully, then site-a. Between the two commits the tunnel simply does not exist; there is no half-configured state to clean up, because WireGuard has no state to leave behind. That property makes a WireGuard rollout unusually safe to stage across two maintenance windows.

The window is for the routing change, not the tunnel. Creating wg0 and its peer affects nothing until a route points at it. Adding the static route is the disruptive step, and it is a single line, which makes the rollback a single line.

Decide the preshared key before deployment, not after. It is cheap to add at build time and awkward later, because adding it is a simultaneous change at both ends with a tunnel outage between the two commits if they are not close together. A peer with none configured behaves as though it had one of all zeroes, so “configured on one side” is a mismatch rather than a partial win — and its only symptom is a handshake that never happens.

Plan for rotation, because nothing will remind you. WireGuard has no expiry, no revocation and no certificate to lapse; a key that leaks stays valid until somebody removes it by hand. Put the key inventory somewhere that gets reviewed, with a date, and treat removing a decommissioned peer’s public key as part of decommissioning it rather than as cleanup.

A tunnel with no traffic looks broken. Handshakes refresh lazily and only when there is something to send, so a quiet tunnel shows an ageing handshake and flat counters — indistinguishable, at a glance, from a failure. Monitor a probe across the tunnel rather than the handshake age, or the first real incident will be preceded by weeks of alerts nobody believed.

Validation

  • show interfaces wireguard wg0 summary on both routers lists the peer with a latest handshake inside the last few minutes and non-zero transfer counters in both directions.
  • The public key each router prints for itself is the value configured as peer ... public-key on the other, and your journal records which key belongs to which router.
  • ping 10.10.10.2 count 3 from site-a returns three replies.
  • ping 192.168.20.1 interface dum0 count 3 returns three replies, and the mirror test from site-b to 192.168.10.1 also does.
  • sudo tcpdump -ni eth1 'udp port 51820' on either router shows UDP in both directions and nothing legible.
  • Your journal holds the Task 5 evidence: initiations arriving at site-b, no reply, and a climbing default-action counter — plus one sentence on why that counter, rather than the capture, was the discriminator.
  • Your journal holds the Task 6 counter reading with sent climbing and received flat, and records that site-b’s wg0 capture was empty while site-a’s was not.
  • Your journal holds the two bracketing ping sizes from Task 7, the MTU derived from them, and which of the two oversized-packet outcomes your hypervisor produced — loss, or fragmentation.
  • Answer in writing: which single command, run on one router, would have looked healthy in all three failures? The answer is why the validation list is nine items and why every one of them names a router.

Expected Outcome

One WireGuard interface per router with one peer, a static route for the far site’s prefix, an MTU set from measurement rather than from the default, and MSS clamping on both ends. site-b carries an input policy that defaults to drop and permits UDP 51820 only from a known peer address, with management permitted ahead of it.

Both routers still hold /config/pre-lab.boot, so the topology resets to its baseline without a rebuild. Your journal holds the key inventory and every capture named in Validation.

Troubleshooting

No latest handshake line at all, on both ends. Nothing has ever completed. Work outwards: capture UDP 51820 on both underlay interfaces to see whether the initiations leave and arrive, then read the receiving router’s input-filter counters, then compare show interfaces wireguard wg0 public-key on each router against the peer ... public-key configured on the other.

A handshake exists but no traffic passes. Read the two transfer counters. Sent climbing with received flat puts the fault on the far router — its allowed-ips, its firewall, or its route back. Neither moving means nothing is being handed to the tunnel here: check show ip route 192.168.20.0/24.

ping 10.10.10.2 works and the payload prefix does not. The tunnel transport is fine and the payload prefix is missing from one of the two lists it has to appear in. Check the static route locally and the peer’s allowed-ips remotely.

Destination host unreachable, immediately, from your own router. The route hands the packet to wg0 and no peer claims the destination. This is the local half of an allowed-ips gap and it is the loud one.

The tunnel comes up and dies whenever it is idle, on one side only. That side is behind a NAT whose UDP binding is expiring. Add set interfaces wireguard wg0 peer NAME persistent-keepalive 25 on the NAT’d side so it keeps the binding alive. It is not needed in this lab, where both routers have their own addresses.

Everything works until you copy a large file. Task 7. Measure the ceiling with size and do-not-fragment, set the interface MTU, clamp the MSS.

Cleanup

Everything here is on an isolated bridge and management was never touched, so cleanup is about leaving a reproducible state.

Step 1. On both routers, load the baseline saved in Task 2:

configure
load /config/pre-lab.boot
compare
commit
save

compare before commit shows exactly what loading the file will change. An empty comparison means you are already at the baseline. Confirm the firewall went with it — show firewall ipv4 input filter on site-b should report no such rule set — because a default-drop input policy left behind on a VM you later reuse for something else is a confusing afternoon.

Step 2. Confirm the tunnel is gone with show interfaces wireguard, which should list nothing.

Step 3. Remove the VMs, then the bridge.

# Run on the Proxmox host, after confirming these IDs with `qm list`.
for VMID in 280 281; do
  qm stop "$VMID"
  qm destroy "$VMID" --purge
done

Then remove the vmbr95 stanza from /etc/network/interfaces and run ifreload -a. Confirm with ip link show vmbr95, which should report that the device does not exist.

What You Learned

  • The handshake and the counters are the tunnel’s whole instrument panel. A recent handshake means the keys and the underlay are good; two moving counters mean traffic is crossing. Everything else in this lab was diagnosis of one of those two readings being wrong.
  • Silence is the design, not a fault. WireGuard never answers what it cannot authenticate and never logs it, so three unrelated failures produced the same symptom and had to be separated by evidence rather than by error messages.
  • A drop counter distinguishes “refused entry” from “refused authentication”. A capture on the ingress interface cannot: it sees the packet before the firewall does.
  • Every prefix is named twice and the two lists fail differently. A missing route is a local error you see immediately; a missing allowed-ips at the far end is silence you can only see from the far end.
  • The default MTU is a guess about someone else’s network. You measured the real ceiling with two pings and set the interface from the measurement — which converted a silent stall into an immediate local error.
  • The MSS clamp is the part that serves the users. Setting the tunnel MTU fixes the router’s own traffic; clamping fixes everybody behind it.

Deliverables

  • · A lab journal recording, for each task, the command run and the output observed
  • · The saved pre-lab configuration file on each router, used by Cleanup
  • · A key inventory: which public key belongs to which router, recorded outside the routers
  • · The transfer-counter capture from the allowed-ips failure, showing bytes moving in one direction only
  • · The measured MTU ceiling from the bisect, with the two ping sizes that bracket it
  • · A written answer to the closing question: which single command would have told you nothing in all three failures?

Verification status

Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.