This is an old revision of the document!
WireGuard
WireGuard is a modern VPN tunnel solution which is quickly taking the place of OpenVPN. Some of the strong key points of WireGuard are, beside a supposedly more secure implementation, the ease of setup and how it simply merge with the overall linux network management.
WireGuard will create an encrypted and protected tunnel between hosts, where each host act as a peer. You need to have at least one host reachable from all the others, of course, but then WireGuard will create a common subnetwork on which all the hosts will see each other.
Concepts
You should be fmailiar with basic networking concepts like routing, subnets, addresses.
A subnet is a portion of a network where all the hosts can ping each other without the need of a gateway. Routing is the act of sending out network packets from a specific network interface toward the destination of the packet. On a subnet, routing is always direct for hosts on the same subnet, otherwise routing happens trough a gateway.
All VPNs work by creating an encrypted tunnel between it's peers. This tunnel needs to be initiated from one host to the other (or viceversa) and whatever traffic flows inside the tunnes is not intellegible to anyone else because it's, guess what, encrypted.
To perform the encryption at both ends, some encryption keys needs to be shared. The approach used by WireGuard is to use the private/public key pairs: each host has one private key (which is by definition, not shared) and public key (which is shared with all the other hosts). The private/public technology ensures that the identity of the host is verified because only the private key can encrypt what the public key can decrypt.
Installation
These steps need to be followed on every host that participate in the WireGuard tunnel.
Installing WireGuard on Gentoo is pretty easy since the latest release is always in portage, but you will also need nftables if you plan to do port-forwarding or any advanced networking:
emerge -v net-vpn/wireguard-tools net-firewall/nftables
Create local host private and public keys:
wg genkey > /etc/wireguard/privatekey wg pubkey < /etc/wireguard/privatekey > /etc/wireguard/publickey chmod 500 /etc/wireguard/privatekey /etc/wireguard/publickey
You will need these two keys for the configuration below.
Configuration
A WireGuard tunnel can connect two or more hosts. A tunnel is usually called wg0 or wg1 and so on. Each tunnel has it's own config file located at /etc/wireguard/wg0.conf. Please note that the following instructions need to be applied to each host in the tunnel.
So, create the tunnel config at /etc/wireguard/wg0.conf:
- wg0.conf
[Interface] PrivateKey = << local host private key >> Address = 10.100.0.1/24 ListenPort = << local host port >> [Peer] PublicKey = << remote host public key >> Endpoint = << remote host public IP >>:<< remote host port >> AllowedIPs = 10.100.0.2/24 # PersistentKeepAlive = 25
Where:
- You can have as many peers as you need to connect to the local host, just create one [Peer] block for each one.
- The PrivateKey is the local host private key
- The Address is the local host address on the tunnel subnetwork (usually, a new subnet you are not using already)
- The ListenPort is the port on which the local host can be reached from the remote hosts. This can be omitted if the local host is not reacheable from the remote hosts
- The PublicKey is the remote host public key
- The Endpoint is the remote host public IP and open port, omit if the remote host cannot be reached from the local host
- The AllowedIPs limits which hosts can send data to the local host, in case you have more than one machine connecting trough the remote host, for example
- The PersistentKeepAlive is usefull to help keep the tunnel connected by sending a keekalive e forcing a reconnection.
Each host connecting to the WireGuard tunnel will need one of these files. These configuration files should usually be symmetrical to each other.
Link the startup scripts and set it to start on boot:
ln -s /etc/init.d/wg-quick /etc/init.d/wg-quick.wg0 rc-update add wg-quick.wg0 default
Remote access
There are tons of WireGuard tutorials online on how to use WireGuard to connect your mobile device securely to your home network, i do not plan to cover this topic here.
Port Forwarding
Using WireGuard for port-forwarding between an external, public accessible host, and an internal non-accessible host (like behind a CGNAT) is a less known topic and i will cover it here.
You will need to use NFTables, see here for more details to better understand the following.
Assumptions:
- You have one internal host which is behind CGNAT, and is not accessible from the internet
- You have one external host which has a public IP address
- Both hosts have WireGuard setup according to the configuration above
- Internal host expose port 22, 80 and 8443
- External host needs to route those three ports to the internal host
- The WireGuard subnet is 10.100.0.0/24 with internal host being 10.100.0.1 and external host 10.100.0.2
- External host public network interface is called enp1s0
- Both hosts WireGuard network iunterface is called wg0
What we need to do is create nftable rules to ensure that:
- packets reaching external host on enp1s0 on port 2022 get routed to 10.100.0.1 port 22
- packets reaching external host on enp1s0 on port 80 get routed to 10.100.0.1 port 80
- packets reaching external host on enp1s0 on port 443 get routed to 10.100.0.1 port 8443
- return packets from the internal host get properly re-routed to the original sender out from enp1s0
Note: see here on why i use port 8443 instead of port 443 on the internal server. This is to differentiate internal connections, which have lesser security, from external connections which get additional SSO layers.
What we need:
- A dedicated table called wg
- A prerouting chain to apply DNAT to incoming packes
- Rules to route port 2022/80/443 to the wg tunnel ports
- A postrouting chain to ensure that all reply packets are properly SNAT back to outside
- Return masquerading rules to ensure the return packets get sent back out of enp1s0
Create the wg table:
nft add table ip wg
Create the base chains:
nft 'add chain ip wg prerouting { type nat hook prerouting priority -100 ; }' nft 'add chain ip wg postrouting { type nat hook postrouting priority 100 ; }'
Create the in-bound rules:
# nft 'add rule ip wg prerouting iifname enp1s0 tcp dport 80 counter' <<- optional for debugging purposes # nft 'add rule ip wg prerouting iifname enp1s0 tcp dport 443 counter' <<- optional for debugging purposes # nft 'add rule ip wg prerouting iifname enp1s0 tcp dport 2022 counter' <<- optional for debugging purposes nft 'add rule ip wg prerouting iifname enp1s0 dnat to tcp dport map { 2022 : 10.100.0.1 . 22 }' nft 'add rule ip wg prerouting iifname enp1s0 dnat to tcp dport map { 80 : 10.100.0.1 . 80 }' nft 'add rule ip wg prerouting iifname enp1s0 dnat to tcp dport map { 443 : 10.100.0.1 . 8443 }'
Create the SNAT return rule (the counter rule is only for debugging, you can omit that rule):
# nft 'add rule ip wg postrouting ip daddr 10.100.0.1 counter' <<- optional for debugging purposes nft 'add rule ip wg postrouting ip daddr 10.100.0.1 masquerade'
This is the resulting NFTables setup:
nft list table wg table ip wg { chain ssh-in { type nat hook prerouting priority dstnat; policy accept; iifname "enp1s0" tcp dport 2022 counter packets 1 bytes 60 iifname "enp1s0" dnat ip to tcp dport map { 2022 : 10.100.0.1 . 22 } } chain ssh-out { type nat hook postrouting priority srcnat; policy accept; ip daddr 10.100.0.1 counter packets 36 bytes 2140 ip daddr 10.100.0.1 masquerade } }
nft add ip wg prerouting 'dnat to tcp dport map { 2022 : 10.70.0.1 . 22 }'