aboutsummaryrefslogtreecommitdiff
path: root/posts/wg-quick-deep-dive/main.md
blob: 5f671fe7a2686af5d8200729b5b90bd88812953c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
`wg-quick(8)` on Linux - a deep dive
====================================

Perhaps you have decided to secure your company's internal database or tunnel all your traffic through a VPN.
You enter the command wg-quick up wg0, watch as commands scroll past your screen, and suddenly realize you
can't access the network.

If this situation sounds familiar, you're not alone. WireGuard is a Layer 3 VPN that has become the de-facto
standard for good reasons — it's fast, simple, and secure. Anecdotally, it's known to be easier to configure
than its bulkier and more convoluted predecessor, OpenVPN. However, configuring or debugging WireGuard networks
requires a robust understanding of networking, something the thorough WireGuard
[documentation](https://www.wireguard.com/#conceptual-overview) can help most users with.

I recently reviewed the source for `wg-quick(8)` and discovered that it was not entirely documented. Some of the documented
parts assumed an in-depth understanding of networking. This research turned out to be a surprisingly
instructive exercise in Linux networking, and I'm sharing my results here. Hopefully, it can fill in some gaps in the
existing documentation.

In particular, this guide:

1. Provides additional exposition on Linux networking
2. Details default-route handling
3. Explains the default firewall configuration
4. Describes the multicast limitations of WireGuard tunnels

The Basics
----------

WireGuard adheres to the Unix philosophy of "doing one thing well," fostering a design of modular systems.
Accordingly, it integrates into the Linux networking stack as a network interface, configured
through the user space tool `wg(8)`. However, to enable traffic flow, IP addresses must be assigned, and
routes need to be configured. While this is a relatively simple procedure, it can become tedious and requires
automation to ensure reliability. This is where `wg-quick(8)` steps in.

> This is an extremely simple script for easily bringing up a WireGuard interface, suitable for a few
common use cases. ... Generally speaking, this utility is just a simple script that wraps invocations
to `wg(8)` and `ip(8)` in order to set up a WireGuard interface. It is designed for users with simple
needs, and users with more advanced needs are highly encouraged to use a more specific tool, a more
complete network manager, or otherwise just use `wg(8)` and `ip(8)`, as usual.
>
> \- `wg-quick(8)`

`wg-quick` serves as a straightforward orchestration tool for creating and removing WireGuard tunnels.
It is the de-facto standard for configuring tunnels since it is cross-platform, supported by all official
clients. Its strength lies in the fact that the configuration provides a complete description of the tunnel,
allowing tunnels to be brought up or down with a single command. Additionally, the interface can be initiated
at boot through systemd, although - oddly - this feature remains undocumented. Without additional tooling, `wg-quick`
can configure either a static server endpoint or a roaming peer.

A "static server endpoint" refers to a computer with a static IP, typically provided by cloud computing vendors,
which functions as a node connecting peers. Often, it may facilitate access to protected resources, like a database.
A roaming peer, on the other hand, is a machine that connects using the static IP of the server and can access the
server or other peers. Peers typically lack a fixed endpoint and "roam" from one IP to another.

There's also a special provision for cases where a peer routes all their internet traffic through the tunnel, aligning
with how most consumers conceive of a VPN. However, VPNs can also selectively transmit traffic bound for specific
computers. Unfortunately, the server configuration for this setup cannot be handled with wg-quick alone since it
necessitates Network Address Translation (NAT). This limitation likely stands as an exception, enabling VPN providers
to distribute `wg-quick` configurations instead of writing their own tools to route traffic.

wg-quick is by no means the sole configuration tool for WireGuard. It's supported alongside other network management
systems such as networkd and NetworkManager. Specifically, NetworkManager is a prevalent tool for managing WiFi networks
on desktop environments, and it has the capability to import WireGuard tunnels using the .conf format, just like wg-quick.
(The extent of feature compatibility between them, however, is something I'm not fully aware of at the moment.) On the
other hand, networkd is more commonly leveraged for network management on servers. In the near future, I'll be releasing a
tool that can "import" (or more accurately, transpile) wg-quick files into networkd configurations. When choosing between
these options, it would be wise to defer to your system's network manager. Most users configure WireGuard tunnels within
the initial network namespace. Multiple tools can end up tripping over one another while
managing the same network resources.

It is also worth stating that `wg-quick` is not the only configuration tool. WireGuard is supported both by
`networkd` and `NetworkManager`. `NetworkManager` is commonly used to manage WiFi networks on the desktop
and can import WireGuard tunnels using the `.conf` format shared with `wg-quick`. (Although I am unaware of
the feature compatibility.) `networkd` is commonly used to manage networks on the server. Soon, I will be
releasing a tool I wrote which allows `wg-quick` files to be "imported" (or more accurately transpiled) to
`networkd`. When considering which option to use, I would default to your system's network manager. WireGuard
tunnels affect resources in the global network namespace, so it makes sense for them to be managed by an entity
responsible for managing network resources globally.

The process of manually configuring a WireGuard tunnel is thoroughly outlined on the [quickstart page](https://www.wireguard.com/quickstart/).
If you've set up a network with static IP addresses, you might already be acquainted with this process. However, many
enthusiasts and developers may have avoided this route, since the aptly named Dynamic Host Configuration Protocol (DHCP)
takes care of automating IP management.  In the following section, I'll delve into configuring network interfaces on Linux,
assuming that you possess a fundamental understanding of networking concepts. Should you be new to setting up a static network,
or if you find yourself unfamiliar with terms like private IP address space, subnets, public IPs, routers, subnet masks, and
interfaces, I strongly recommend taking the time to acquaint yourself with these concepts before proceeding.

### Revisiting a Simple Network

WireGuard seamlessly integrates with the Linux networking stack, functioning as a networking interface. The Linux networking
system is indeed robust, supporting a wide array of features. However newcomers should be warned, the documentation often appears
to lag behind the pace of feature development. In this article, we'll explore the fundamentals of a simple Local Area Network (LAN)
and shed light on how packets are routed on contemporary Linux systems. This foundational knowledge is essential for understanding
how `wg-quick` interfaces with these routing constructs.

Assigning a static IP address to a networking interface can be accomplished using the following command. This sets up a LAN with the
subnet `192.168.0.0-127` and assigns the networking interface the IP address `192.168.0.22`:

```
[#] ip -4 addr add 192.168.0.22/25 dev eth0
```

Should you attempt to transmit data to a peer at `192.168.0.11` (e.g., using a UDP socket), how does the kernel determine where to
route these packets? Upon executing the command above, the kernel modifies a structure known as the routing table. Routing tables
function as a database, directing traffic to a specific interface by matching the destination IP address. Most routes are automatically
appended to the `main` routing table, which can be viewed with the command `ip route show`:

```
[#] ip -4 route show table main

192.168.0.0/25 dev eth0 proto kernel scope link src 192.168.0.22
```

In essence, packets destined for our subnet will be sent using eth0 with the source IP address `192.168.0.22`. The kernel
automatically adds this route for a subnet assigned to a link, known as a prefix route. If multiple competing routes
exist in a routing table, the route is selected using the longest prefix match algorithm. Essentially, the most specific
route, characterized by the longest subnet mask, is chosen. If you add a custom route specifying that `192.168.0.11/32`
should be sent using eth1, traffic will flow through this interface instead, since the 32-bit subnet mask is longer than
the 25-bit subnet mask:

```
[#] ip -4 route add 192.168.0.11/32 dev eth1
[#] ip -4 route get 192.168.0.11
192.168.0.11 dev eth1 src 192.168.0.12 uid 0
    cache
```

This brings up an intriguing question: If we send traffic to the current host at `192.168.0.22`, how does the routing
algorithm determine that the packets shouldn't leave the computer but instead be handled by local programs? The answer
lies in another table that precedes the `main` table, known as the `local` table. The `local` table is consulted before
the `main` table, and if a match is found, the packet is routed accordingly. The kernel also manipulated this table in
response to our earlier `ip addr add` command.

```
[#] ip -4 route show table local

local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1
local 192.168.0.22 dev eth0 proto kernel scope host src 192.168.0.22
broadcast 192.168.0.127 dev eth0 proto kernel scope link src 192.168.0.22
```

The original ip addr add command added both a local route and a broadcast route. Specifically, the entry `local 192.168.0.22 dev eth0`
stipulates that packets addressed to `192.168.0.22` should be "looped back" and delivered to sockets listening locally on `eth0`, or
bound to the address `192.168.0.22`. Typically, IP traffic is unicast, meaning it originates with one host and is directed at another.
The `broadcast` rule informs the kernel that `192.168.0.127` is a broadcast address. By default, the kernel utilizes the highest IP
address in the subnet as the broadcast address.

The above overview offers almost a complete picture, but there's another crucial layer of indirection in play: the Routing Policy Database
(RPDB). The RPDB acts as a guide, specifying how routing tables are selected and ordered. This can be queried with the ip rule command, and
the initial state of the RPDB might look something like this:

```
[#] ip rule

0:	from all lookup local
32766:	from all lookup main
32767:	from all lookup default
```


Quoting directly from the manual provides a comprehensive explanation:

> Each policy routing rule consists of a selector and an action predicate.  The RPDB is scanned in order of decreasing priority (note that a lower number means
higher priority, see the description of PREFERENCE below). The selector of each rule is applied to {source address, destination address, incoming interface,
tos, fwmark} and, if the selector matches the packet, the action is performed. The action predicate may return with success.  In this case, it will either give
a route or failure indication and the RPDB lookup is terminated. Otherwise, the RPDB program continues with the next rule.
>
> Semantically, the natural action is to select the nexthop and the output device.
>
> At startup time the kernel configures the default RPDB consisting of three rules:
>
> 1. Priority: 0, Selector: match anything, Action: lookup routing table local (ID 255).  The local table is a special routing table containing high priority
> control routes for local and broadcast addresses.
> 2. Priority:  32766,  Selector: match anything, Action: lookup routing table main (ID 254).  The main table is the normal routing table containing all non-
>  policy routes. This rule may be deleted and/or overridden with other ones by the administrator.
> 3. Priority: 32767, Selector: match anything, Action: lookup routing table default (ID 253).  The default table is empty. It is reserved for some post-pro‐
 cessing if no previous default rules selected the packet.  This rule may also be deleted.
>
> \- ip-rule(8)

If you've been following along, you now understand that when a socket binds or connects to an address, the kernel
selects an appropriate route by querying the routing tables. This query is guided by the rules specified in the
routing policy database (RPDB). When a table contains multiple routes, the longest prefix match algorithm is used
to determine the most specific route, and that is the one selected.

_Note:_ Policy routing can be bypassed using the `SO_BINDTODEVICE` option. This allows you to bind a socket directly
to a specific device, effectively sidestepping the standard routing process.

### Bringing Up a Site-to-Site-Style Tunnel with `wg-quick(8)`

> Use up to add and set up an interface, and use down to tear down and remove an interface. Running up
adds a WireGuard interface, brings up the interface with the supplied IP addresses, sets up mtu and
routes, and optionally runs pre/post up scripts.
>
> \- wg-quick(8)

The command `wg-quick` first searches for a configuration file. If the first argument to `wg-quick up` matches
the pattern `[a-zA-Z0-9_=+.-]{1,15}`, which represents a valid interface name on Linux, the file is assumed
to exist in `/etc/wireguard` with the provided name and a `.conf` extension. Once a suitable file is found,
its contents are read. For example, it might read the following file, `wg0.conf`:

```
[Interface]
PrivateKey = yDdqzxdE66e64xy5Qu1PshT0ybQJLHbU9N+91PS1Dng=
Address = 192.168.42.1/24

[Peer]
PublicKey = o837llPmQ4t9cN0rmiLasp6SF54dAzS0Ea1p71c1jFA=
AllowedIPs = 192.168.42.0/32
Endpoint = 19.216.242.139:16262

[Peer]
PublicKey = YIUKiCiw9+6an3HnDn7t3CwlF30ERQkhEQ6f3jRBUnk=
AllowedIPs = 10.1.0.0/16
Endpoint = 19.216.242.138:16263
```

The process begins identically to the steps taken in the quick start documentation. A WireGuard interface is
added to the system with the interface name from the corresponding file:

```
ip link add dev wg0 type wireguard
```

Then, the WireGuard configuration is obtained by stripping out the `wg-quick`-specific sections, passing the
rest to the `wg` command. This configuration can be produced using the `wg-quick strip` command. Running
`wg-quick strip` on the above example removes the Address section, and wg parses the configuration to pass it
on to the kernelspace driver [1].

[1] It can also communicate this information to the userspace implementation, if available.

```
[#] wg-quick strip wg0.conf
[Interface]
PrivateKey = yDdqzxdE66e64xy5Qu1PshT0ybQJLHbU9N+91PS1Dng=

[Peer]
PublicKey = o837llPmQ4t9cN0rmiLasp6SF54dAzS0Ea1p71c1jFA=
AllowedIPs = 192.168.42.0/32
Endpoint = 19.216.242.139:16262

[Peer]
PublicKey = YIUKiCiw9+6an3HnDn7t3CwlF30ERQkhEQ6f3jRBUnk=
AllowedIPs = 10.1.0.0/16
Endpoint = 19.216.242.138:16263
```

As the official documentation states:

> The configuration file adds a few extra configuration values to the format understood by wg(8) in order
to configure additional attributes of an interface. It handles the values that it understands, and then
it passes the remaining ones directly to wg(8) for further processing.
>
> \- wg-quick(8)

The way the interface configuration affects packet routing is well-covered by the
[original documentation](https://www.wireguard.com/#simple-network-interface). If you haven't read it, you
should explore the "Simple Network Interface" and "Cryptokey Routing" sections before continuing.

```
[#] wg-quick strip wg0.conf > wg0-stripped.conf
[#] ip setconf wg0 wg0-stripped.conf
```

Next, the IP addresses specified in Addresses are added to the interface. For the above configuration,
it assigns the IP address `192.168.42.1` to the WireGuard interface. This becomes the source IP address
for packets traveling to the peer VPN endpoint [3]. The system also automatically creates a prefix route
directing traffic from any IP address matching `192.168.42.0/24` to the interface `wg0`, and a local route
for `192.168.42.1`.

[3] The source IP address, as defined in the Addresses section of the configuration, serves as the originating
address for packets sent to the peer VPN endpoint. However, this can be overridden using a "raw socket," in which
an application has the ability to define all fields of a packet, including the source IP. While this introduces
significant security considerations, WireGuard's Cryptorouting scheme substantially mitigates the risks. Malicious
peers are restricted to spoofing only those addresses listed in the `AllowedIPs` section of the `[Peer]` configuration,
and all unauthorized packets are promptly dropped. This built-in mechanism aids in containing potential threats.

```
[#] ip -4 addr add 192.168.42.1/24 dev wg0
```

Afterward, the link is brought up, and the MTU (Maximum Transmission Unit) is set. The MTU represents the maximum size
of a packet that can be communicated on a link. If unspecified, the MTU of the underlying link is obtained, and the MTU
of the WireGuard tunnel is reduced by 80 bytes to exclude the overhead of the WireGuard encapsulation packets. I looked
into this and am still unsure as to why an 80 byte MTU reduction was chosen. My working theory is that this value was
chosen to cover both IPv4 and IPv6 with a factor of safety [4].

[4] The encapsulation method depends on whether the tunnel endpoint is an IPv4 address or an IPv6 address. The maximum
size of an IPv4 packet is 60 bytes. IPv6 packets are more challenging to quantify since they can have any number of
arbitrary header extensions. The base IPv6 header is only 40 bytes but additional headers can add up. Accordingly, 80
bytes would cover an IPv6 header with a few extensions.


```
[#] ip link set mtu up 1420 dev wg0
```

Finally, routes are added for all the entries of `AllowedIPs` to the `main` routing table:

```
[#] ip -4 route add 192.168.42.0/32 dev wg0
[#] ip -4 route add 10.1.0.0/16 dev wg0
```

### Default-Route Handling

The above example illustrates how WireGuard establishes static routes for specific segments of the IP space, which we
specified in `AllowedIPs`. However, these previous examples assumed that the peer endpoint falls outside any of the
`AllowedIP` ranges. If this were not true, the routes would create a routing loop, with packets exiting the WireGuard
interface being routed back into it.

One common use case for a VPN is to route all traffic to an endpoint which functions as a NAT router. (If you are unfamiliar
with NAT, picture your home router home router.) This can enhance privacy, reduce the specificity of a user's public IP address
in fingerprinting systems, prevent ISPs from selling data to advertisers, or obscure the regional ISP an individual is using.
To route all traffic, we need a default route `0.0.0.0/0` and `::0/0` which match all destination IPs. This creates an issue
since our endpoint's IP will fall into this range. Fortunately, there are methods to make default routes work as intended.

Linux policy routing can be used in various ways to solve this problem, and the
[Wireguard Routing and Namespace Documentation](https://www.wireguard.com/netns/#routing-all-your-traffic) outlines a few potential
solutions. `wg-quick` elegantly accomplishes this with the following rules, which require some explanation:


```
[#] wg set wg0 fwmark 51820
[#] ip -4 route add 0.0.0.0/0 dev wg0 table 51820
[#] ip -4 rule add not fwmark 51820 table 51820
[#] ip -4 rule add table main suppress_prefixlength 0
```

The Linux kernel routing system features `fwmark`, an integer marker ranging from `0` to `2^32 - 1` which can be attached to a packet.
It designates that the packet should be routed or filtered according to special rules. Packets with this mark can either be routed to
a specific table using policy routing (`ip-rule`) or filtered with the `nftables` framework. The `fwmark` can be set either within the
`netfilter` subsystem or by a program when making a network connection [4].

[4] `setsockopt` can be used with `SO_MARK` to set the outgoing mark as so long as the process has the `CAP_NET_ADMIN` capability.

In this instance, wg-quick marks all packets leaving the tunnel with the `fwmark` 51820:

```
[#] wg set wg0 fwmark 51820
```

Recall our previous discussion of routing tables. `wg-quick` creates a new routing table within which the default route directs all
traffic to the `wg0` interface, effectively serving as a "default gateway." With none of the mask bits set, all traffic is routed to `wg0`:

```
[#] ip -4 route add 0.0.0.0/0 dev wg0 table 51820
```

Traffic must then be directed to this routing table. Here, all traffic that has not exited the VPN interface (and thus does not have
this fwmark) is sent to the table with the default route:

```
[#] ip -4 rule add not fwmark 51820 table 51820
```

By default, the IP command assigns this rule a higher priority than the rule querying the main table.

```
[#] ip rule
0:	from all lookup local
32765:	not from all fwmark 0xca6c lookup 51820
32766:	from all lookup main
32767:	from all lookup default
```

We have established that traffic will be directed through the Wireguard interface if it does not match any local IP ranges. It will
then be sent through `wg0` before being routed according to the `main` table (and likely exit through a default gateway.)

The `ip rule` command prints the mark in hex as `0xca6c`.

#### Handling Local Network Traffic

While VPN traffic will flow as expected, this configuration may have unintended consequences. All local LAN traffic will be directed
to the tunnel, preventing access to the local network (except for encrypted Wireguard packets directed to the default gateway).
Policy routing has one more trick:

```
[#] ip -4 rule add table main suppress_prefixlength 0
```

The command `suppress_prefixlength` rejects all routes with prefixes equal to or less than the specified length. So, `suppress_prefixlength 0`
rejects all default gateways, causing the routing algorithm to query the main table first, which typically contains LAN routes but can also
contain routes setup by virtualisation software.

These rules are set whenever the default IPv4 `0.0.0.0/0` or `IPv6 ::/0` routes appear in the `AllowedIPs`. In the case of IPv6, `ip -6` rules
are added as well. Finally, if `FwMark` is not specified, `wg-quick` searches for the next available mark, stopping when an empty routing table
is found. Finally, the `FwMark` is set to the routing table number.

### The Default Firewall

wg-quick configures a firewall if nftables is installed and a default route is specified. The following firewall rules are
setup when an IPv4 endpoint is specified:

```
table ip wg-quick-wg0 {
	chain preraw {
		type filter hook prerouting priority raw; policy accept;
		iifname != "wg0" ip daddr 192.168.42.1 fib saddr type != local drop
	}

	chain premangle {
		type filter hook prerouting priority mangle; policy accept;
		meta l4proto udp meta mark set ct mark
	}

	chain postmangle {
		type filter hook postrouting priority mangle; policy accept;
		meta l4proto udp meta mark 0x0000ca6c ct mark set meta mark
	}
}
```

The first rule in the preraw chain mitigates a potential vulnerability that could allow a network attacker to access servers
listening on `wg0`. It's easy to assume that local services listening on the WireGuard IP address are safe since they're only
accessible to those connected to the tunnel. However, this is not the case.

Imagine an attacker connected on `eth0`, an adjacent ethernet interface, attempting to access the address assigned to our WireGuard
interface, `192.168.42.1`. Since a route for this address is in the `local` table, the attacker can send packets to `192.168.42.1`
and receive responses, even if the requests originate from another subnet. This could enable the attacker to exfiltrate sensitive
data or inject malicious packets. Therefore, the first rule enforces that services on `wg0` can only be accessed through addresses on
the same interface.

#### Reverse Path Forwarding

The last two rules in the code snippet above pertain to reverse path forwarding, a technique used to prevent spoofed packets from entering
a network. Consider a router with forwarding enabled and two links:

```
172.30.1.0/31 dev eth-01 proto kernel scope link src 172.30.1.0
172.30.2.0/31 dev eth-02 proto kernel scope link src 172.30.2.0
```

Now, think about a scenario where a router connected to `eth-02` with IP `172.30.2.1` and an attacker connected to this router attempt to perform
a denial-of-service attack against `1.1.1.1` by spoofing network ICMP packets with random destination IPs and the source address of their victim.
Classically, if one of these spoofed packets "from" `1.1.1.1` destined for `172.30.2.1` arrives on `eth-01`, it would be forwarded. However, this
would be suspicious since a packet from `172.30.2.1` destined for `1.1.1.1` would be dropped, not forwarded through `eth-02`. By considering if a
routable path exists in the reverse direction, a router can automatically drop spoofed packets. This concept is known as reverse path forwarding,
as defined by RFC 3704, known as "strict reverse path forwarding."

To enable strict reverse path filtering in the Linux kernel, you can use the following sysctl switch:

```
[#] sysctl net.ipv4.conf.INTERFACE_NAME.rp_filter=1
```

Most Linux distributions default to Loose Reverse Path Forwarding. In strict mode, traffic from `172.30.1.1` on eth-02 would be dropped, as it should
have routed through `eth-01`. This enforces a symmetrical routing policy but can disrupt asymmetric routing configurations. Loose Reverse Path Forwarding,
conversely, processes traffic if routable through any interface.

Why does this matter for WireGuard? A firewall's secondary set of rules ensures that strict reverse path forwarding operates accurately. The `fwmark` is
set by the traffic traversing the reverse path, working in collaboration with Linux's conntrack subsystem, which persistently associates incoming traffic
with its corresponding outgoing traffic [5]. The second rule sets the "connection mark" labeled `ct mark` as the `fwmark` during connection establishment,
while the third rule copies it back when packets are received.

`wg-quick` then instructs the kernel to use the `fwmark` for reverse path forwarding, since this is not enabled by default:

```
[#] sysctl -q net.ipv4.conf.all.src_valid_mark=1
```

[5] For example, in tracking a UDP stream, both IPs (source and destination) and L4 headers (source and destination ports) can be tracked to identify
a connection.

## Additional Thoughts

When researching WireGuard and reading through the documentation, I found there were a few non-obvious
technical details that seemed undocumented, although potentially important.

### UDP Socket Parameters

WireGuard permits users to modify the UDP port on which an interface listens using `ListenPort`. Although,
it always binds its internal UDP socket to all available interfaces using `INADDR_ANY` [6]. This is an
important consideration reasoning about packet flows and writing netfilter rules.

### Broadcast and Multicast Traffic

Broadcast and multicast traffic are used in standard protocols, notably service autodiscovery. When considering how
to configure effective firewalls, I was led to ask whether WireGuard carried multicast traffic. Short answer: yes and
no. If the multicast ip range `224.0.0.0/4` or `ff00::/8` appear in the `AllowedIPs` for the tunnel, multicast traffic
could theoretically be broadcast from the other peer. Although, this works with at most two peers. Internally, WireGuard
uses a single prefix tree to select the endpoint to which a packet is sent. If the same `AllowedIPs` appear within
the configuration of multiple peers, the last peer configuration overwrites the routes configured on other peers [7].
Thus Mutlicast traffic can be directed to, at most, one peer. Thus, if you want to pass broadcast or multicast traffic,
a secondary encapsulation method must be used on top of WireGuard. This does not mean peers with default routes in their
`AllowedIPs` may leak Multicast traffic. This seems to be technically possible.

In practice, the interface flags are used by programs like `avahi` to determine on which interfaces they should broadcast [8].
WireGuard is not started with the `IFF_MULTICAST` or `IFF_BROADCAST` flags [8]. According to comments in the Linux kernel,
links without `IFF_MULTICAST` can perform multicast but point-to-point devices cannot broadcast [9]. The exact meaning of these
flags seem ambiguous from the limited documentation, and I wouldn't be surprised if the kernel and userspace developers were
on different pages as to their exact technical meaning.

> `IFF_MULTICAST` means that this media uses special encapsulation for multicast frames. Apparently, all `IFF_POINTOPOINT` and
`IFF_BROADCAST` devices are able to use multicasts too.
>
> \- [9]

[6] [Kernel cGit](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/net/wireguard/socket.c?id=e74216b8def3803e98ae536de78733e9d7f3b109#n361)
    [Github](https://github.com/torvalds/linux/blob/89bf6209cad66214d3774dac86b6bbf2aec6a30d/drivers/net/wireguard/socket.c#L361)

[7] [Github](https://github.com/lathiat/avahi/blob/55d783d9d11ced838d73a2757273c5f6958ccd5c/avahi-core/iface-linux.c#L104)

[8] [Kernel cGit](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/net/wireguard/device.c?id=b5cc3833f13ace75e26e3f7b51cd7b6da5e9cf17#n292)
    [Github](https://github.com/torvalds/linux/blob/b5cc3833f13ace75e26e3f7b51cd7b6da5e9cf17/drivers/net/wireguard/device.c#L292)

[9] [Kernel cGit](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/if_link.h?id=706a741595047797872e669b3101429ab8d378ef#n423)
    [Github](https://github.com/torvalds/linux/blob/706a741595047797872e669b3101429ab8d378ef/include/uapi/linux/if_link.h#L423)