Migrate 'WireGuard Packet Forwarding' to hugo
This commit is contained in:
parent
cb32f81189
commit
25dff2d3f8
|
@ -0,0 +1,327 @@
|
|||
---
|
||||
title: WireGuard Packet Forwarding
|
||||
date: 2023-08-21
|
||||
sn_id: 230179
|
||||
---
|
||||
|
||||
# introduction
|
||||
|
||||
In my [previous blog post](/demystifying-wireguard-and-iptables), I have shown you how to setup your own VPN with [WireGuard](https://wireguard.com/) and [`iptables`](https://wiki.archlinux.org/title/iptables). We have established a point-to-point connection between two peers where one peer (10.172.16.1) was reachable from the internet:
|
||||
|
||||
![point-to-point.png](./point-to-point.png)
|
||||
|
||||
Today, I will explain how more peers can be added to our VPN.
|
||||
One peer ("the router") will be configured to forward packets between all other peers ("the end devices").
|
||||
Therefore, our VPN will become a star network:
|
||||
|
||||
![star-network.png](./star-network.png)
|
||||
|
||||
You could then [install WireGuard on your mobile dev](https://www.wireguard.com/install/#android-play-store-direct-apk-file) and reach all other machines in your VPN from anywhere with internet connection.
|
||||
|
||||
To get a deeper understanding how this forwarding works, we will take a brief look at the network traffic with `tcpdump` after we configured our network.
|
||||
|
||||
# initial configuration
|
||||
|
||||
The end devices will start with a point-to-point connection to the router with 10.172.16.1 as its internal IP address.
|
||||
|
||||
The WireGuard and firewall configuration for these could therefore look like this:
|
||||
|
||||
_WireGuard configuration for end device:_
|
||||
|
||||
```
|
||||
[Interface]
|
||||
Address = 10.172.16.x/32
|
||||
PrivateKey = <PRIVATE KEY OF PEER>
|
||||
|
||||
[Peer]
|
||||
AllowedIPs = 10.172.16.1/32
|
||||
PublicKey = <PUBLIC KEY OF ROUTER>
|
||||
Endpoint = 139.162.153.133:51913
|
||||
PersistentKeepalive = 25
|
||||
```
|
||||
|
||||
_firewall configuration for end device:_
|
||||
|
||||
```
|
||||
-P INPUT DROP
|
||||
-P FORWARD DROP
|
||||
-P OUTPUT DROP
|
||||
-A INPUT -m state --state ESTABLISHED -j ACCEPT
|
||||
-A INPUT -i wg0 -j ACCEPT
|
||||
-A INPUT -s 139.162.153.133 -i enp3s0 -p udp -m udp --sport 51913 -j ACCEPT
|
||||
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
|
||||
-A OUTPUT -o wg0 -j ACCEPT
|
||||
-A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
|
||||
```
|
||||
|
||||
Since the router is connected to multiple peers, it will have multiple `[Peer]` sections in its configuration:
|
||||
|
||||
_WireGuard configuration for router:_
|
||||
|
||||
```
|
||||
[Interface]
|
||||
Address = 10.172.16.1/32
|
||||
PrivateKey = <PRIVATE KEY OF ROUTER>small
|
||||
ListenPort = 51913
|
||||
|
||||
[Peer]
|
||||
AllowedIPs = 10.172.16.2/32
|
||||
PublicKey = <PUBLIC KEY OF PEER>
|
||||
|
||||
[Peer]
|
||||
AllowedIPs = 10.172.16.4/32
|
||||
PublicKey = <PUBLIC KEY OF PEER>
|
||||
|
||||
...
|
||||
|
||||
[Peer]
|
||||
AllowedIPs = 10.172.16.25/32
|
||||
PublicKey = <PUBLIC KEY OF PEER>
|
||||
```
|
||||
|
||||
_firewall configuration for router:_
|
||||
|
||||
|
||||
```
|
||||
-P INPUT DROP
|
||||
-P FORWARD DROP
|
||||
-P OUTPUT DROP
|
||||
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
|
||||
-A INPUT -i wg0 -j ACCEPT
|
||||
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
|
||||
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
|
||||
-A OUTPUT -o wg0 -j ACCEPT
|
||||
```
|
||||
|
||||
<!-- TODO: I used to use <small> for this but my current setup omits raw HTML. -->
|
||||
|
||||
_If these configurations are confusing to you, read my [previous blog post](/demystifying-wireguard-and-iptables)._
|
||||
|
||||
# end device configuration
|
||||
|
||||
The only change we have to do on the end devices is to route all IP addresses within the VPN to the router peer. We configure this in the WireGuard configuration file at _/etc/wireguard/wg0.conf_:
|
||||
|
||||
```diff
|
||||
[Peer]
|
||||
- AllowedIPs = 10.172.16.1/32
|
||||
+ AllowedIPs = 10.172.16.0/24
|
||||
PublicKey = <PUBLIC KEY OF ROUTER>
|
||||
Endpoint = 139.162.153.133:51913
|
||||
PersistentKeepalive = 25
|
||||
```
|
||||
|
||||
To apply these changes, we run this command[^1]:
|
||||
|
||||
```
|
||||
$ wg syncconf wg0 <(wg-quick strip wg0)
|
||||
```
|
||||
|
||||
[^1]: `wg-quick strip wg0` returns the config in the format that `wg` can parse.
|
||||
This is necessary because `wg-quick` "adds a few extra configuration values to the format understood by `wg` in order to
|
||||
configure additional attributes of an interface". ([source](https://man.archlinux.org/man/wg-quick.8.en#CONFIGURATION))
|
||||
|
||||
# router configuration
|
||||
|
||||
## firewall
|
||||
|
||||
The router requires no additional WireGuard configuration.
|
||||
|
||||
However, the router firewall needs to allow forwarding packets inside the VPN:
|
||||
|
||||
```diff
|
||||
-P INPUT DROP
|
||||
-P FORWARD DROP
|
||||
-P OUTPUT DROP
|
||||
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
|
||||
-A INPUT -i wg0 -j ACCEPT
|
||||
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
|
||||
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
|
||||
-A OUTPUT -o wg0 -j ACCEPT
|
||||
+ -A FORWARD -i wg0 -o wg0 -j ACCEPT
|
||||
```
|
||||
|
||||
## kernel
|
||||
|
||||
We also need to allow IP forwarding in the kernel. We can configure kernel parameters with `sysctl`.
|
||||
|
||||
To see the current value for IP forwarding:
|
||||
|
||||
```
|
||||
$ sysctl net.ipv4.ip_forward
|
||||
net.ipv4.ip_forward = 0
|
||||
```
|
||||
|
||||
This basically does the same as checking the content of the file _/proc/sys/net/ipv4/ip\_forward_:
|
||||
|
||||
```
|
||||
$ cat /proc/sys/net/ipv4/ip_forward
|
||||
0
|
||||
```
|
||||
|
||||
To change the setting, we can use `sysctl -w`:
|
||||
|
||||
```
|
||||
$ sysctl -w net.ipv4.ip_forward=1
|
||||
```
|
||||
|
||||
However, this change is not persistent. To make sure the new setting is kept after reboot, we need to modify _/etc/sysctl.conf_. We can do this by appending `net.ipv4.ip_forward = 1` to the file:
|
||||
|
||||
```
|
||||
$ echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
|
||||
```
|
||||
|
||||
To reload settings:
|
||||
|
||||
```
|
||||
$ sysctl -p
|
||||
```
|
||||
|
||||
# inspecting the network traffic with tcpdump
|
||||
|
||||
We now should have a connection between every peer via the router.
|
||||
|
||||
To confirm this, we ping one machine (10.172.16.25) from another machine (10.172.16.6):
|
||||
|
||||
```
|
||||
$ ping 10.172.16.25
|
||||
PING 10.172.16.25 (10.172.16.25) 56(84) bytes of data.
|
||||
64 bytes from 10.172.16.25: icmp_seq=1 ttl=63 time=138 ms
|
||||
64 bytes from 10.172.16.25: icmp_seq=2 ttl=63 time=165 ms
|
||||
64 bytes from 10.172.16.25: icmp_seq=3 ttl=63 time=182 ms
|
||||
64 bytes from 10.172.16.25: icmp_seq=4 ttl=63 time=206 ms
|
||||
64 bytes from 10.172.16.25: icmp_seq=5 ttl=63 time=229 ms
|
||||
```
|
||||
|
||||
We will inspect this traffic on 10.172.16.1 (the router) and 10.172.16.6 (the pinging machine) with `tcpdump` now.[^2]
|
||||
|
||||
[^2]: Since 10.172.16.25 is a mobile device, I didn't include `tcpdump` output from that device. It also wouldn't include anything interesting.
|
||||
|
||||
On the pinging machine, we will get the following output for the virtual network interface[^3]:
|
||||
|
||||
[^3]: I used `-t` to not include timestamps and `-n` to not resolve IP addresses to hostnames. `-i <interface>` selects the interface we want to tap.
|
||||
|
||||
```
|
||||
$ tcpdump -tni wg0
|
||||
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
|
||||
listening on wg0, link-type RAW (Raw IP), snapshot length 262144 bytes
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 19, seq 1, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 19, seq 1, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 19, seq 2, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 19, seq 2, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 19, seq 3, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 19, seq 3, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 19, seq 4, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 19, seq 4, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 19, seq 5, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 19, seq 5, length 64
|
||||
```
|
||||
|
||||
and this for the physical network interface (filtered by UDP packets from/to port 51913):
|
||||
|
||||
```
|
||||
$ tcpdump -tni enp3s0 'udp and port 51913'
|
||||
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
|
||||
listening on enp3s0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
|
||||
IP 192.168.178.146.51941 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 192.168.178.146.51941: UDP, length 128
|
||||
IP 192.168.178.146.51941 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 192.168.178.146.51941: UDP, length 128
|
||||
IP 192.168.178.146.51941 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 192.168.178.146.51941: UDP, length 128
|
||||
IP 192.168.178.146.51941 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 192.168.178.146.51941: UDP, length 128
|
||||
IP 192.168.178.146.51941 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 192.168.178.146.51941: UDP, length 128
|
||||
```
|
||||
|
||||
On the router, we get this output for the virtual network interface:
|
||||
|
||||
```
|
||||
$ tcpdump -tni wg0
|
||||
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
|
||||
listening on wg0, link-type RAW (Raw IP), snapshot length 262144 bytes
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 1, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 1, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 1, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 1, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 2, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 2, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 2, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 2, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 3, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 3, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 3, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 3, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 4, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 4, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 4, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 4, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 5, length 64
|
||||
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 5, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 5, length 64
|
||||
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 5, length 64
|
||||
```
|
||||
|
||||
and this for the physical network interface:
|
||||
|
||||
```
|
||||
$ tcpdump -tni eth0 'udp and port 51913'
|
||||
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
|
||||
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
|
||||
IP 87.161.X.X.51941 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 109.43.X.X.11007: UDP, length 128
|
||||
IP 109.43.X.X.11007 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 87.161.X.X.51941: UDP, length 128
|
||||
IP 87.161.X.X.51941 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 109.43.X.X.11007: UDP, length 128
|
||||
IP 109.43.X.X.11007 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 87.161.X.X.51941: UDP, length 128
|
||||
IP 87.161.X.X.51941 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 109.43.X.X.11007: UDP, length 128
|
||||
IP 109.43.X.X.11007 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 87.161.X.X.51941: UDP, length 128
|
||||
IP 87.161.X.X.51941 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 109.43.X.X.11007: UDP, length 128
|
||||
IP 109.43.X.X.11007 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 87.161.X.X.51941: UDP, length 128
|
||||
IP 87.161.X.X.51941 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 109.43.X.X.11007: UDP, length 128
|
||||
IP 109.43.X.X.11007 > 139.144.78.247.51913: UDP, length 128
|
||||
IP 139.144.78.247.51913 > 87.161.X.X.51941: UDP, length 128
|
||||
```
|
||||
|
||||
As you can see, for every ICMP echo request, we get an ICMP echo reply from 10.172.16.25 in the `tcpdump` output of 10.172.16.6. We also see that we receive a response for every UDP packet sent to port 51913 of 139.144.78.247.
|
||||
|
||||
However, in the `tcpdump` output of 10.172.16.1 for `wg0`, we see every packet twice. Why is that?
|
||||
|
||||
The reason for this is that `tcpdump` captures every incoming and outgoing packet.[^4]
|
||||
|
||||
[^4]: To be precise, `tcpdump` captures incoming packets _before_ firewall processing while outgoing packets will be captured _after_ firewall processing. This means `tcpdump` will capture incoming packets that will be dropped by the firewall whereas outgoing packets that were dropped will not show up. See [here](https://wiki.archlinux.org/title/Network_Debugging#Tcpdump) and [here](https://superuser.com/a/925332) for more information.
|
||||
|
||||
Since we are forwarding packets on the virtual network interface, the packet for 10.172.16.25 from 10.172.16.6 will be captured on 10.172.16.1 when it enters the interface from 10.172.16.6 and when it leaves to 10.172.16.25.
|
||||
|
||||
The same happens when the response from 10.172.16.25 arrives at 10.172.16.1 and is forwarded to 10.172.16.6.
|
||||
|
||||
We can see this forwarding happening better in the `tcpdump` output for the physical network interface. There, we see that for every ICMP echo request, we see one UDP packet from 87.161.X.X to 139.144.78.247 and then another from 139.144.78.247 to 109.43.X.X.
|
||||
|
||||
As you also can see, the forwarding does not change internal IP addresses. The packets still contain the same source and destination IP addresses. This works because we have configured the interfaces of all peers to route all IP addresses towards the router peer.
|
||||
So when 10.172.16.25 sees the ICMP echo request from 10.172.16.6 via 10.172.16.1, it will just respond to 10.172.16.6 again via 10.172.16.1.
|
||||
|
||||
The UDP packet IP addresses do seem changed, though. However, this is not network address translation (NAT) but just packet encapsulation. As mentioned in my [previous blog post](/demystifying-wireguard-and-iptables), the physical network interfaces have no notion of internal IP addresses thus every UDP packet is sent using publicly routable IP addresses over the wire.
|
||||
|
||||
This means we are not doing any network address translation here hence the name forwarding.
|
||||
|
||||
Another important detail is that we do not need the following FORWARD rules in the router:
|
||||
|
||||
```
|
||||
-A FORWARD -s 10.172.16.0/24 -i wg0 -o eth0 -j ACCEPT
|
||||
-A FORWARD -d 10.172.16.0/24 -i eth0 -o wg0 -j ACCEPT
|
||||
```
|
||||
|
||||
These rules are only required if we want to forward VPN traffic to `eth0` which faces the public internet. Therefore, these rules would be part of a configuration to give internet access via 10.172.16.1 to peers inside the VPN.
|
||||
|
||||
---
|
||||
|
||||
Thanks for reading my second blog post! If you want to read more content like this, please consider subscribing via [RSS](https://dev.ekzyis.com/blog/rss.xml).
|
||||
|
||||
In the next blog post, we will use network address translation with the SNAT and DNAT targets in `iptables` for port forwarding. This will make it possible to expose an internal service (like an HTTP server for example) to the public internet.
|
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
Loading…
Reference in New Issue