VyOSXLI · WireGuardWireGuard
WireGuard firewall and outer-packet routing — UDP port, input chain, multi-WAN
What you'll learn
- Place the WireGuard listener rule in the correct base chain (`input filter`, not `forward filter`)
- Filter tunnelled traffic with a named rule-set reached by `action jump` from `forward filter`
- Steer the encrypted outer packet onto a chosen uplink with `policy local-route`
- State what VyOS 1.5 does not expose (a firewall rule action that sets a packet mark) and what replaces it
- Recognise the production failure modes of WireGuard firewall configuration
Prerequisites
Verified against VyOS 1.5.x LTS (circinus) · VyOS 1.4.x (sagitta) — legacy · FRRouting 10.x (VyOS 1.5) · Linux kernel 6.6 LTS (VyOS 1.5 base) · strongSwan 5.9.x (IPsec) · WireGuard 1.0.x (kernel module + userspace tooling)
WireGuard crosses a firewall twice, and the two crossings are different
packets in different chains. The outer packet is a UDP datagram
addressed to the router itself (default port 51820); it is terminated
locally, so it is judged by the input chain. The inner packets are
the tunnelled traffic that appears on wg0 after decryption; they are
routed onwards, so they are judged by the forward chain. An operator
who writes one rule and expects it to cover both has a half-configured
router.
This lesson covers the firewall configuration for both crossings, and then the harder problem: on a multi-WAN router, choosing which uplink the encrypted outer packet leaves by. That turns out not to be a firewall question at all.
The listener rule — the input chain
WireGuard listens on a UDP port. The peer must be able to reach that port or the handshake never completes and the tunnel never comes up.
The packet carrying the handshake is addressed to the router’s own WAN
address, so on VyOS 1.5 it is filtered by firewall ipv4 input filter —
the base chain for traffic terminating on the router:
configure
# The set of peers allowed to talk to our listener
set firewall group address-group WIREGUARD-PEERS address 198.51.100.2
# Return traffic for sessions the router itself opened
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
# The WireGuard listener
set firewall ipv4 input filter rule 10 action accept
set firewall ipv4 input filter rule 10 description 'WireGuard handshake and data 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 eth0
set firewall ipv4 input filter default-action drop
set firewall ipv4 input filter default-log
commit
save
Three things in that block are worth naming, because each of them is a place where a VyOS 1.3 habit produces a rule that does not do what the operator thinks:
- The chain is
input filter, not a named rule-set. The router is the destination of the outer packet. A rule-set attached to “the WAN interface” in the abstract does not exist in VyOS 1.5; the base chain is chosen by where the packet is going, and the interface is a match condition inside it (inbound-interface name eth0). - The interface binding is a match, not an interface-node setting.
VyOS 1.3 wrote
set interfaces ethernet eth0 firewall in name WAN-IN. That node does not exist in 1.4 or 1.5. Attempting it fails atcommitwith a configuration-path error, which is the good case; the bad case is an operator who deletes the failing line and believes the remaining rule-set is doing something. stateis a value, not a node with a toggle. The 1.5 form isstate established, one value perset. There is noenableleaf underneath it.
Filtering the tunnelled traffic — a named rule-set and a jump
Once the tunnel is up, the decrypted inner packets arrive on the wg0
interface and are routed towards the LAN. They are forwarded traffic,
so they belong to firewall ipv4 forward filter.
The idiomatic VyOS 1.5 structure is to keep the policy in a named
rule-set and reach it with action jump, which is what replaced the
1.3 per-interface binding:
configure
# The policy for traffic arriving from the tunnel
set firewall ipv4 name TUNNEL-IN default-action drop
set firewall ipv4 name TUNNEL-IN default-log
set firewall ipv4 name TUNNEL-IN description 'From the WireGuard peer towards our LAN'
set firewall ipv4 name TUNNEL-IN rule 5 action accept
set firewall ipv4 name TUNNEL-IN rule 5 state established
set firewall ipv4 name TUNNEL-IN rule 5 state related
set firewall ipv4 name TUNNEL-IN rule 10 action accept
set firewall ipv4 name TUNNEL-IN rule 10 description 'Remote site to local LAN'
set firewall ipv4 name TUNNEL-IN rule 10 source address 192.168.50.0/24
set firewall ipv4 name TUNNEL-IN rule 10 destination address 192.168.1.0/24
set firewall ipv4 name TUNNEL-IN rule 100 action drop
set firewall ipv4 name TUNNEL-IN rule 100 state invalid
set firewall ipv4 name TUNNEL-IN rule 100 log
# Reach the policy from the forward base chain
set firewall ipv4 forward filter rule 10 action jump
set firewall ipv4 forward filter rule 10 jump-target TUNNEL-IN
set firewall ipv4 forward filter rule 10 inbound-interface name wg0
set firewall ipv4 forward filter default-action drop
commit
save
The jump is the whole point of the 1.4/1.5 restructure. A named
rule-set is a chain with no packets in it until something sends packets
to it; the forward filter rule is what selects the traffic (here: it
arrived on wg0) and hands it over. Rule ordering in the base chain
therefore matters as much as ordering inside the named set.
Note what default-action drop inside TUNNEL-IN means: a packet that
enters the named set and matches none of its rules is dropped there and
does not return to forward filter. If you want unmatched traffic to
fall back to the base chain, the named set’s default action must be
return, not drop.
The outer packet is locally originated — and that changes everything
Now the harder half. A router with two uplinks runs a tunnel over each, and the operator wants tunnel 1 to leave by ISP-A and tunnel 2 by ISP-B. The instinct is to mark the traffic in the firewall and route on the mark. On VyOS 1.5 that instinct fails twice.
First: VyOS 1.5 firewall rules do not set marks. The action values
available on a firewall rule are accept, continue, drop, jump,
queue, reject, return and synproxy. None of them writes a packet
mark. Marking lives in the policy tree, not the firewall tree:
set policy route <name> rule <n> set mark <1-2147483647>
— and a policy route rule set is applied to an interface with
set policy route <name> interface <interface>, where it sees traffic
arriving on that interface.
Second: the encrypted outer packet never arrives on an interface.
It is generated by the kernel’s WireGuard module on the router itself.
Ingress policy routing does not see locally originated traffic at all,
so no amount of correct policy route configuration will steer it.
flowchart TD
LAN["Inner packet from LAN<br/>192.168.1.0/24 -> 192.168.50.0/24"]
PBR["policy route on eth1<br/>(sees this packet)"]
WG["Routed into wg0<br/>WireGuard encrypts"]
OUT["Outer packet<br/>UDP 51820 -> peer endpoint<br/>LOCALLY ORIGINATED"]
LR["policy local-route<br/>(the only hook that sees this)"]
FIB["Routing table chosen here"]
ISP["Leaves via ISP-A or ISP-B"]
LAN --> PBR
PBR --> WG
WG --> OUT
OUT --> LR
LR --> FIB
FIB --> ISP
The hook that does see locally originated traffic is
policy local-route. It is a rule in the kernel’s routing-policy
database, evaluated for packets the router itself sources, and it
selects a routing table:
set policy local-route rule <1-32765> protocol <protocol>
set policy local-route rule <1-32765> source address <address>
set policy local-route rule <1-32765> destination address <address>
set policy local-route rule <1-32765> destination port <1-65535>
set policy local-route rule <1-32765> source port <1-65535>
set policy local-route rule <1-32765> fwmark <1-2147483647>
set policy local-route rule <1-32765> set table <main|1-200>
Multi-WAN: one tunnel per uplink
Give each tunnel its own listen port. That is the selector that makes the two outer packet streams distinguishable, and it costs nothing:
configure
# Tunnel over ISP-A. The install form stages the private key in this
# session; without one the commit fails with
# "Wireguard private-key not defined".
run generate pki wireguard key-pair install interface wg1
set interfaces wireguard wg1 address 10.10.10.1/30
set interfaces wireguard wg1 description 'Site B over ISP-A'
set interfaces wireguard wg1 port 51821
set interfaces wireguard wg1 peer site-b address 198.51.100.2
set interfaces wireguard wg1 peer site-b port 51820
set interfaces wireguard wg1 peer site-b public-key 'cVn4T2sM8xQ6yB1hJ0dR7kL3pW9zA5eG2uY8iO4nX1c='
set interfaces wireguard wg1 peer site-b allowed-ips 10.20.0.0/16
# Tunnel over ISP-B
run generate pki wireguard key-pair install interface wg2
set interfaces wireguard wg2 address 10.10.20.1/30
set interfaces wireguard wg2 description 'Site C over ISP-B'
set interfaces wireguard wg2 port 51822
set interfaces wireguard wg2 peer site-c address 203.0.113.2
set interfaces wireguard wg2 peer site-c port 51820
set interfaces wireguard wg2 peer site-c public-key 'kyxTG3RVQnG8mgi3oXIwBZBh45yNjHtYmojy4t1qr84='
set interfaces wireguard wg2 peer site-c allowed-ips 10.30.0.0/16
# The inner routes. allowed-ips does not install any.
set protocols static route 10.20.0.0/16 interface wg1
set protocols static route 10.30.0.0/16 interface wg2
# One routing table per uplink
set protocols static table 100 route 0.0.0.0/0 next-hop 198.51.100.254
set protocols static table 200 route 0.0.0.0/0 next-hop 203.0.113.254
# Steer each tunnel's outer packet by its source port
set policy local-route rule 100 protocol udp
set policy local-route rule 100 source port 51821
set policy local-route rule 100 set table 100
set policy local-route rule 200 protocol udp
set policy local-route rule 200 source port 51822
set policy local-route rule 200 set table 200
commit
save
The peer node is a name, not a key: site-b is an operator-chosen
label and the key goes in the public-key leaf beneath it. That has
been true since 1.3, and so has the split of the far end into separate
address and port leaves — the interface code joins them back into
wg’s single endpoint <address>:<port> argument, which is why commit
insists on both or neither. The combined Endpoint = host:port you may
have seen belongs to WireGuard’s own wg0.conf file format, not to the
VyOS CLI. The only WireGuard rename between 1.3 and 1.4 was the peer’s
key leaf, from pubkey to public-key.
If you would rather key the rules on the far end than on your own listen
port, destination address works equally well and is arguably clearer
when a peer’s endpoint is stable:
set policy local-route rule 100 protocol udp
set policy local-route rule 100 destination address 198.51.100.2
set policy local-route rule 100 destination port 51820
set policy local-route rule 100 set table 100
Either way the rule is evaluated on the router’s own transmit path, so the routing table — and therefore the uplink — is chosen before the packet reaches an interface.
Validation
# The chains, with per-rule counters
show firewall ipv4 input filter
show firewall ipv4 forward filter
show firewall ipv4 name TUNNEL-IN
show firewall statistics
# The tunnel itself
show interfaces wireguard
show interfaces wireguard wg1
show interfaces wireguard wg1 summary
# The alternate tables and the local-route rules
show ip route table 100
show ip route table 200
show configuration commands | match 'policy local-route'
# The wire
sudo tcpdump -ni eth0 'udp port 51821' -c 4
sudo tcpdump -ni eth1 'udp port 51822' -c 4
A clean result looks like this: show interfaces wireguard wg1 summary
reports a recent latest handshake against site B’s public key — that
command is sudo wg show wg1 on 1.5, so it identifies the peer by key
rather than by the name site-b; the input-chain rule 10 counter
is advancing; show ip route table 100 contains exactly the default
route you put there; and the two tcpdump captures each see their own
tunnel and only their own tunnel. If both captures see both tunnels, the
local-route rules are not matching and every outer packet is following
the main table.
Production failure modes
The listener rule is in the wrong chain
The operator writes the UDP 51820 rule into forward filter. The
counter never advances, because the outer packet terminates on the
router and never reaches the forward hook.
Diagnostic: show firewall ipv4 forward filter shows a zero counter on
the WireGuard rule while show firewall ipv4 input filter shows the
default-drop counter climbing at the handshake retry interval.
Fix: move the rule to input filter.
The named rule-set is defined but nothing jumps to it
TUNNEL-IN exists, its rules are correct, and its counters are all
zero. Nothing in forward filter selects traffic into it. This is the
commonest 1.3-to-1.5 migration failure, because in 1.3 the rule-set was
activated by the interface node and there was no separate step to
forget.
Diagnostic: show firewall ipv4 name TUNNEL-IN shows zero on every
rule including the default action; show firewall ipv4 forward filter
has no jump-target TUNNEL-IN.
Fix: add the action jump rule with the appropriate
inbound-interface name.
The named set drops what should have fallen through
TUNNEL-IN has default-action drop, and a legitimate flow that the
operator expected a later forward filter rule to permit is dropped
inside the named set instead.
Diagnostic: the TUNNEL-IN default-action counter is advancing while
the later forward filter rules stay at zero.
Fix: decide deliberately. default-action return sends unmatched
traffic back to the base chain; default-action drop makes the named
set terminal. Both are valid designs, but only one of them is yours.
Both tunnels leave by the same uplink
The local-route rules exist but do not match, so every outer packet
uses the main table.
Diagnostic: sudo tcpdump -ni eth1 'udp port 51822' is silent while
sudo tcpdump -ni eth0 sees both port 51821 and port 51822.
Fix: check the selector. The commonest cause is matching on
source address when the router has not yet chosen a source address —
source selection happens after the routing decision, so a rule keyed
on the source address of a locally originated packet is matching
something that does not exist yet. Key on protocol plus port, or on the
destination address.
The tunnel works in one direction
Traffic from the LAN reaches the remote site; replies do not come back.
Diagnostic: show firewall ipv4 name TUNNEL-IN shows the default-drop
counter advancing at the same rate as the outbound flow.
Fix: the state established / state related rule is missing or is
ordered after a rule that already dropped the packet. Conntrack admits
the reply only if a rule matches it.
TCP through the tunnel stalls on large transfers
Small requests work; a file transfer hangs. This is the classic MTU/MSS symptom, not a firewall symptom — WireGuard’s encapsulation overhead reduces the usable path MTU, and if ICMP “fragmentation needed” is being filtered somewhere upstream, path-MTU discovery cannot correct it.
Fix: clamp MSS on the tunnel interface with ip adjust-mss (and
ipv6 adjust-mss for IPv6). Note the name: tcp-mss was the VyOS 1.3
spelling and is not accepted on 1.5.
Rollback
configure
compare
# Undo the firewall pieces
delete firewall ipv4 input filter rule 10
delete firewall ipv4 forward filter rule 10
delete firewall ipv4 name TUNNEL-IN
# Undo the uplink steering
delete policy local-route rule 100
delete policy local-route rule 200
commit
save
Two cautions on this rollback. Deleting firewall ipv4 input filter rule 10 while default-action drop remains in place closes the
listener — the tunnel stops rather than opening up. And deleting the
local-route rules does not break connectivity; it silently moves both
tunnels onto the main table’s uplink, which is the failure that is hard
to notice. Use commit-confirm 5 for either change when you are working
through the tunnel you are about to modify.
Production discipline
Cross-course references
- Part XLI-04 (
XLI-VyOS-WireGuard/ routing) covers the routing of the inner packets, includingallowed-ipsas a routing statement. - Part XXXVII-01 (
XXXVII-VyOS-Firewall/ stateful vs stateless) covers the conntrack states these rules match on. - Part XXXIX-04 (
XXXIX-VyOS-MultiWAN/ policy routing) covers thepolicy routetree for transit traffic, the counterpart to thepolicy local-routetree used here. - Part XIV-04 (
XIV-VyOS-MultiTables/ table source bind) covers the alternate routing tables referenced byset table.
Quiz
Knowledge check · 4 questions
Q1. On VyOS 1.5, which base chain filters the encrypted WireGuard outer packet arriving from a peer, and what is the default listen port?
Q2. On VyOS 1.5 you can give a firewall rule an action that sets a packet mark, then match that mark in a policy rule to choose which uplink the encrypted WireGuard outer packet takes.
Q3. An operator configures WireGuard and sets `firewall ipv4 input filter default-action drop`. The tunnel does not establish and the peer never acquires a handshake. Walk the diagnosis on VyOS 1.5 and give the fix.
R1 and R2 both have WireGuard configured with matching keys and allowed-ips. R1's input chain has a default action of drop and no rule permitting UDP 51820. R2 sends handshake initiations to R1 every few seconds. WireGuard emits no error and no ICMP rejection, so the only local symptom is the missing handshake timestamp.
Q4. An operator has two uplinks and two WireGuard tunnels (wg1 intended over ISP-A, wg2 over ISP-B). Both tunnels' encrypted outer packets leave via ISP-A. The operator has written firewall rules intended to mark each tunnel's traffic. What is wrong, and what is the supported VyOS 1.5 configuration?
The operator's design was: a firewall rule marks wg1 traffic, a second marks wg2 traffic, and policy rules route on the marks. On VyOS 1.5 the firewall rules have no mark action to begin with, so nothing is ever marked. Even a correct `policy route` mark would not help, because `policy route` is applied to an interface and evaluates traffic arriving on it, whereas the encrypted outer packet is generated by the kernel on the router itself and never arrives on any interface.
Passing score: 75%. Answers are checked in this browser.