4-hop Wireguard chained VPN with RHEL9 and FirewallD
Overview
A multi-node chained VPN creates a tunnel through a given number of endpoints. Packets are encrypted when they leave your local computer and then another layer of encryption is added at each next hop.
This type of setup can be useful for clients in highly restrictive environments where online access to the outside world is limited. Your ISP can see the entry node you are connecting to but not the content of your packets, nor the exit node.
Furthermore if an attacker has compromised one of the endpoints and utilized a deanonymizing software, due to the layered encryption they will not be able to see the destination or source of your packets nor will they be able to see their contents.
You will need 4 VMs running RHEL9 or one of it’s derivatives. The further away geographically these endpoints are in, will also require better and better network connection.
We will implement the following architecture:
To avoid confusion this article will refer to the nodes by their domain names rather than IP addresses. Nodes wg01, wg02, wg03 will have 2 wireguard interfaces. Node wg04 will have a single interface.
This article will follow the below naming convention and wireguard private addresses:
wg01.mreja
- endpoint01: 10.88.10.1 # Used by local wireguard client (10.88.10.2) as initial entrypoint
- client01: 10.88.20.2 # Used as client for wg02
wg02.mreja
- endpoint02: 10.88.20.1 # Used by the wg01 client01 interface
- client02: 10.88.30.2 # Used as client for wg03
wg03.mreja
- endpoint03: 10.88.30.1 # Used by the wg02 client02 interface
- client: 10.88.30.2 # Used as client for wg04
wg04.mreja
- endpoint04: 10.88.40.1 # Used by the wg03 client03 interface
This setup has been implemented within a LAN but is also 100% applicable in the internet.
Step 1: Prepare the servers
Important
Run the following command on all 4 wg0*.mreja servers
Update the system and install the required packages:
# yum update -y ; yum install -y vim bind-utils traceroute unbound firewalld wireguard-tools
Reboot the server:
# systemctl reboot
Backup the unbound configuration:
# mv /etc/unbound/unbound.conf /etc/unbound/unbound.confBACK
Download the root.hints file for unbound:
# curl --output /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache
Create a new unbound configuration:
# vim /etc/unbound/unbound.conf
Enter the following contents and save the file:
server:
num-threads: 4
#Enable logs
verbosity: 1
# chrootdir
chroot: ""
#list of Root DNS Server
root-hints: "/var/lib/unbound/root.hints"
#Use the root servers key for DNSSEC
auto-trust-anchor-file: "/var/lib/unbound/root.key"
#trust-anchor-file: /etc/unbound/trusted-key.key
#Respond to DNS requests on all interfaces
interface: 0.0.0.0
max-udp-size: 3072
#Authorized IPs to access the DNS Server
access-control: 0.0.0.0/0 refuse
access-control: 127.0.0.1 allow
access-control: 10.88.0.0/16 allow
#not allowed to be returned for public internet names
private-address: 10.88.0.0/16
# Hide DNS Server info
hide-identity: yes
hide-version: yes
#Limit DNS Fraud and use DNSSEC
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
#Add an unwanted reply threshold to clean the cache and avoid when possiblea DNS Poisoning
unwanted-reply-threshold: 10000000
#Have the validator print validation failures to the log.
val-log-level: 1
#Minimum lifetime of cache entries in seconds
cache-min-ttl: 1800
#Maximum lifetime of cached entries
cache-max-ttl: 14400
prefetch: yes
prefetch-key: yes
Check the unbound configuration for syntax errors:
# unbound-checkconf
Start the unbound service:
# systemctl start unbound
Amend the name servers for the OS and save the changes:
# nmcli connection modify ens18 ipv4.ignore-auto-dns true
# nmcli connection modify ens18 ipv4.dns 127.0.0.1
# nmcli connection up ens18
Verify that DNS is working:
# dig yahoo.com | grep "IN.*A\|SERVER"
You should see 127.0.0.1#53 in the SERVER section and also see all the yahoo.com IP addresses:
;yahoo.com. IN A
yahoo.com. 1757 IN A 98.137.11.163
yahoo.com. 1757 IN A 98.137.11.164
yahoo.com. 1757 IN A 74.6.231.20
yahoo.com. 1757 IN A 74.6.143.25
yahoo.com. 1757 IN A 74.6.231.21
yahoo.com. 1757 IN A 74.6.143.26
;; SERVER: 127.0.0.1#53(127.0.0.1)
Step 2: Configure Wireguard
Important
Run the following commands on wg01, wg02, wg03. Replace endpoint01 and client01 with endpoint02/endpoint03 and client02/client03 for wg02/wg03 respectively.
Configure the wireguard directories and keys:
# cd /etc/wireguard/
# umask 0022
# mkdir endpoint01 client01
# cd endpoint01/
# wg genkey | tee private.key | wg pubkey > public.key
# cd ../client01/
# wg genkey | tee private.key | wg pubkey > public.key
Create an additional routing table.
Wireguard will use this to add routing rules after the interface is up. These rules will ensure traffic is properly routed through the wireguard tunnels.
# echo "1 wgroute" >> /etc/iproute2/rt_tables
Important
Run the following commands on wg04.
Configure the wireguard directories and keys:
# cd /etc/wireguard/
# umask 0022
# mkdir endpoint01
# cd endpoint01/
# wg genkey | tee private.key | wg pubkey > public.key
Step 3: Create the wireguard interface configurations
After creating an interface file make sure to create a symlink of in it’s parent directory. This will help you referencing the configuration file with tools such as wg-quick and systemd.
For example, after creating endpoint01 on wg01, run the following:
# ln -s /etc/wireguard/endpoint01/endpoint01.conf /etc/wireguard/endpoint01.conf
For each server create the interfaces as shown below.
wg01
/etc/wireguard/endpoint01/endpoint01.conf
[Interface]
Address = 10.88.10.1/24
SaveConfig = true
ListenPort = 51821
PrivateKey = <private key endpoint01>
[Peer]
PublicKey = <local client public key>
AllowedIPs = 10.88.10.2/32
/etc/wireguard/endpoint01/client01.conf
[Interface]
Address = 10.88.20.2/32
PrivateKey = <private key client01>
DNS=10.88.40.1
PostUp = wg set client01 peer <public key endpoint02> allowed-ips 0.0.0.0/0 && ip route add 0.0.0.0/0 dev client01 table wgroute && ip rule add from 10.88.10.2/32 lookup wgroute
[Peer]
PublicKey = <public key endpoint02>
Endpoint = wg02.mreja:51822
AllowedIPs = 10.88.20.1/32
PersistentKeepalive = 21
wg02
/etc/wireguard/endpoint01/endpoint02.conf
[Interface]
Address = 10.88.20.1/24
SaveConfig = true
ListenPort = 51822
PrivateKey = <private key endpoint02>
[Peer]
PublicKey = <public key client01>
AllowedIPs = 10.88.0.0/16
/etc/wireguard/endpoint01/client02.conf
[Interface]
Address = 10.88.30.2/32
PrivateKey = <private key client02>
DNS=10.88.40.1
PostUp = wg set client02 peer <public key endpoint03> allowed-ips 0.0.0.0/0 && ip route add 0.0.0.0/0 dev client02 table wgroute && ip rule add from 10.88.10.2/32 lookup wgroute && ip rule add from 10.88.20.2/32 lookup wgroute
[Peer]
PublicKey = <public key endpoint03>
Endpoint = wg03.mreja:51823
AllowedIPs = 10.88.30.1/32
PersistentKeepalive = 21
wg03
/etc/wireguard/endpoint01/endpoint03.conf
[Interface]
Address = 10.88.30.1/24
SaveConfig = true
ListenPort = 51823
PrivateKey = <private key endpoint03>
[Peer]
PublicKey = <public key client02>
AllowedIPs = 10.88.0.0/16
/etc/wireguard/endpoint01/client03.conf
[Interface]
Address = 10.88.40.2/32
PrivateKey = <private key client03>
DNS=10.88.40.1
PostUp = wg set client03 peer <public key endpoint04> allowed-ips 0.0.0.0/0 && ip route add 0.0.0.0/0 dev client03 table wgroute && ip rule add from 10.88.10.2/32 lookup wgroute && ip rule add from 10.88.20.2/32 lookup wgroute && ip rule add from 10.88.30.2/32 lookup wgroute
[Peer]
PublicKey = <public key endpoint04>
Endpoint = wg04.mreja:51824
AllowedIPs = 10.88.40.1/32
PersistentKeepalive = 21
wg04
/etc/wireguard/endpoint01/endpoint04.conf
[Interface]
Address = 10.88.40.1/24
SaveConfig = true
ListenPort = 51824
PrivateKey = <private key endpoint04>
[Peer]
PublicKey = <public key client03>
AllowedIPs = 10.88.0.0/16
local client endpoint
/etc/wireguard/client-name.conf
[Interface]
Address = 10.88.10.2/24
DNS = 10.88.40.1
PrivateKey = <local client private key>
PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
[Peer]
PublicKey = <public key endpoint01>
AllowedIPs = 0.0.0.0/0
Endpoint = wg01.mreja:51821
Step 4: Configure FirewallD
Important
Add the following firewall rule on wg01, wg02, wg03. Replace endpoint01 and client01 with endpoint02/endpoint03 and client02/client03 for wg02/wg03 respectively:
# firewall-cmd --add-interface=endpoint01 --add-interface=client01 --permanent
Add the below firewall rule wg04:
# firewall-cmd --add-interface=endpoint04 --permanent
Add the following firewall rules on all 4 wireguard nodes. Make sure to replace WGPORT with each of the ports chosen within the endpoint0*.conf files!
# firewall-cmd --add-port=WGPORT/udp --permanent
# firewall-cmd --add-service=dns --add-service=ssh --permanent
# firewall-cmd --add-masquerade --permanent
# firewall-cmd --add-forward --permanent
# firewall-cmd --reload
Step 5: Start all the things
Start the wireguard interfaces in the following order:
wg04
# wg-quick up endpoint04
wg03
# wg-quick up endpoint03
# wg-quick up client03
wg02
# wg-quick up endpoint02
# wg-quick up client02
wg01
# wg-quick up endpoint01
# wg-quick up client01
local client
# wg-quick up client-name
To ensure the wireguard interfaces are started on boot use systemd to enable each interface on each node. For example to enable endpoint01 for wg01 run:
# systemctl enable wg-quick@endpoint01.service
Once the all interfaces have been started and you have connected to the wireguard VPN from your local client check if everything works as expected.
Verify you can ping wg01, wg02, wg03, wg04:
# ping -c2 10.88.10.1
# ping -c2 10.88.20.1
# ping -c2 10.88.30.1
# ping -c2 10.88.40.1
Verify you can reach the internet:
# ping 1.1.1.1
Confirm a traceroute to the internet goes through all 4 nodes:
# traceroute 1.1.1.1
The beginning of the output should look as such:
traceroute to 1.1.1.1 (1.1.1.1), 30 hops max, 60 byte packets
1 10.88.10.1 (10.88.10.1) 2.762 ms 2.730 ms 2.719 ms
2 10.88.20.1 (10.88.20.1) 2.707 ms 2.696 ms 2.670 ms
3 10.88.30.1 (10.88.30.1) 4.186 ms 4.182 ms 4.171 ms
4 10.88.40.1 (10.88.40.1) 4.162 ms 4.151 ms 4.148 ms
...
Check if DNS resolution is working and going through the wg04 interface:
# dig a yahoo.com | grep "IN.*A\|SERVER"
You should see the yahoo.com ip addresses and the SERVER line should contain: 10.88.40.1#53
.
;yahoo.com. IN A
yahoo.com. 1795 IN A 98.137.11.164
yahoo.com. 1795 IN A 74.6.231.20
yahoo.com. 1795 IN A 74.6.231.21
yahoo.com. 1795 IN A 74.6.143.25
yahoo.com. 1795 IN A 74.6.143.26
yahoo.com. 1795 IN A 98.137.11.163
;; SERVER: 10.88.40.1#53(10.88.40.1)
If you check your public IP while connected to the tunnel you should see the public IP of the wg04 node:
# curl ifconfig.me
The End
If you have followed this article you should have successfully set up a 4 node multi-hop chained VPN. Traffic routed through the tunnel will be layered encryption. Before your request reach the internet they should be routed through all 4 hops.
If you’ve enjoyed this, make sure to go ahead and look at the Articles section. My personal projects you can find on my git server. If you have a question or want to get in touch, feel free to email me.
Thank you for reading and have a good night!