The VPN server can be handy in different situations:
- when you need a static IP address and traffic encryption (traveling, mobile internet, etc.)
- hide your traffic from your home ISP
- If your home ISP doesn’t provide a static IP address
- you want to use IPv6 but ISP supports only IPv4
- and more…
VPN can be bought from a VPN service like NordVPN, but this is not an exclusive VPN server. One of their servers handles hundreds of connections and they can’t provide static IP and port forwarding. Some of the servers can get banned on Google or YouTube. The best solution is to buy VPS and set up your VPN server.
Guide Scope
In this guide I will explain (or at least try) how to configure Debian 11:
- WireGuard server
- WireGuard client
- DNS server
- Traffic and port forwarding with nftables
Prerequisites
We need VPS with SSH access and Linux OS.
VPS
For VPN purposes we don’t need crazy powerful VPS. Even a machine with 256 Mb of RAM and 1 vCPU will work. Search only for VM-based VPS (like Amazon, DigitalOcean, Google, etc.). We need access rights to network interfaces + a way to install WireGuard kernel modules.
OS
I used Debian 11 for my VPN server. Ubuntu Server LTS is good too (20.04 or 22.04). It should work on other distributions with modern kernels, but package names can differ.
WireGuard server setup
We will start from the server side of our VPN.
Install packages
Debian 11 has needed packages in repositories. Simply install them.
sudo apt install wireguard wireguard-tools
Generate server private key and public keys
Run this command to generate server public and private keys.
wg genkey | tee server_private_key | wg pubkey > server_public_key
Create server config
Create a file called /etc/wireguard/wg0.conf on the server and add the following content.
[Interface]
Address = 172.16.0.1/24
Address = fdf5:6028:947d:1234::1/64
SaveConfig = true
PrivateKey = <insert server_private_key>
ListenPort = 51820
Where 172.16.0.2-172.16.0.254
is our IPv4 private range (if you need more than 253 addresses, use /16 mask and 172.16.0.2-172.16.254.254
address range). You can use any standard IPv4 private range: 10.0.0.0/8
, 172.16.0.0/12
, 192.168.0.0/16
. For IPv6 private subnet generation you can use this service.
This will create a wg0 network interface for our VPN.
Enable VPN startup
Enable the WireGuard interface on the VPN server as follows:
sudo chown -v root:root /etc/wireguard/wg0.conf
sudo chmod -v 600 /etc/wireguard/wg0.conf
sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0.service
The last line will enable the auto startup of the interface on the system boot.
Confirm that you have network interface wg0 by running ip a:
wg0: flags=209<UP,POINTOPOINT,RUNNING,NOARP> mtu 1420
inet 172.16.0.1 netmask 255.255.255.0 destination 172.16.0.1
inet6 fdf5:6028:947d:1234::1 prefixlen 64 scopeid 0x0<global>
unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 1000 (UNSPEC)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
WireGuard client setup
WireGuard has clients for all major platforms. In this guide, we will use console setup on Ubuntu 22.04. On other platforms, the config file will be the same.
Install packages
Install the same packages as for the server.
sudo apt install wireguard wireguard-tools
Generate client private key and public keys
Run this command to generate client public and private keys.
wg genkey | tee client_private_key | wg pubkey > client_public_key
Create client config
Create a file called /etc/wireguard/wg0-client.conf on the client and add the following content.
[Interface]
Address = 172.16.0.2/32, fdf5:6028:947d:1234::2/128
PrivateKey = <insert client_private_key>
DNS = 172.16.0.1, fdf5:6028:947d:1234::1
[Peer]
PublicKey = <insert server_public_key>
Endpoint = <insert vpn_server_address>:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 21
Similar to the server case, wg0-client.conf will result in an interface named wg0-client so you can rename the file if you want something different.
AllowedIPs = 0.0.0.0/0, ::/0
will allow and route all IPv4 and IPv6 traffic on the client through the VPN tunnel. This can be narrowed down if you only want some traffic routed over the VPN.
DNS = 172.16.0.1, fdf5:6028:947d:1234::1
will set the DNS resolver IP to our VPN server. This is important to prevent DNS leaks when on the VPN.
Address = 172.16.0.2/32, fdf5:6028:947d:1234::2/128
is an IPv4 and IPv6 address of this client.
Add Peer to the server
For each client, you have to add a [Peer]
section to the server-side config. wg command allows you to add it without manual editing. Run this command on the server machine to add our client:
sudo wg set wg0 peer <client_public_key> allowed-ips 172.16.0.2/32,fdf5:6028:947d:1234::2/128
Connect to the server
Run this command to enable a VPN connection:
sudo wg-quick up wg0-client
Check that it works
The ping should work now so let’s try to ping the server’s local IP.
ping -c 4 172.16.0.1
DNS server setup
DNS queries can leak information about sites we visited so we need our own secure private caching DNS server for our VPN. I checked several options and found that unbound is a good fit.
Its features are:
- Lightweight and fast
- Easy to install and configure
- Security oriented
- Supports DNSSEC
Let’s proceed with it.
Installation
Install unbound from a repository.
sudo apt install unbound
Download the root DNS servers list and save it.
sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache
Configuration
Create /etc/unbound/unbound.conf.d/server.conf file with this content:
server:
num-threads: 4
#Enable logs
verbosity: 1
#list of Root DNS Server
root-hints: "/var/lib/unbound/root.hints"
#Respond to DNS requests on all interfaces
interface: 0.0.0.0
interface: ::0
max-udp-size: 3072
#Authorized IPs to access the DNS Server
access-control: 0.0.0.0/0 refuse
# localhost
access-control: 127.0.0.1/8 allow
access-control: ::1/128 allow
access-control: 1::1/64 allow
# VPN
access-control: 172.16.0.0/24 allow
access-control: fdf5:6028:947d:1234::/64 allow
#not allowed to be returned for public internet names
#localhost
private-address: 127.0.0.1/8
private-address: ::1/128
private-address: 1::1/64
# VPNs
private-address: 172.16.0.0/24
private-address: fdf5:6028:947d:1234::/64
# 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
# interfaces
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
#Add an unwanted reply threshold to clean the cache and avoid when possible a 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
Start it
We should enable the unbound service to autostart and start the DNS server.
sudo systemctl enable unbound
sudo systemctl start unbound
Test it
Let’s check that it responds properly. Should return IP addresses of www.google.com.
nslookup www.google.com 172.16.0.1
If it returns IP addresses then we are good!
Traffic Forwarding
New Linux distros moved to the nftables firewall interface so we will use them to set up proper forwarding.
Install it
We need to install the nft
program first.
sudo apt install nftables
Enable forwarding in the sysctl
Edit the file /etc/sysctl.conf and set the following lines as:
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
Then reload the config without reboot:
sudo sysctl -p
Configure forwarding
Edit /etc/nftables.conf file like this:
#!/usr/sbin/nft -f
flush ruleset
define DEV_VPN = wg0
define DEV_WORLD = eth0
define IP_WORLD_V4 = <insert your server IPv4>
define IP_WORLD_V6 = <insert your server IPv6>
define PORT_VPN = 51820
define DEV_LOCAL_NETS = { $DEV_VPN }
define DEV_OUT_NETS = { $DEV_WORLD }
table inet global {
map port_forwards_tcp_ipv4 {
type ipv4_addr . inet_service : ipv4_addr . inet_service
# lets forward port for our torrent. Our 12345 to 12345 on 172.16.0.2
elements = { $IP_WORLD_V4 . 12345 : 172.16.0.2 . 12345 }
}
map port_forwards_tcp_ipv6 {
type ipv6_addr . inet_service : ipv6_addr . inet_service
# lets forward port for our torrent. Our 12345 to 12345 on fdf5:6028:947d:1234::2
elements = { $IP_WORLD_V6 . 12345 : [fdf5:6028:947d:1234::2] . 12345}
}
map port_forwards_udp_ipv4 {
type ipv4_addr . inet_service : ipv4_addr . inet_service
# lets forward port for our torrent. Our 12345 to 12345 on 172.16.0.2
elements = { $IP_WORLD_V4 . 12345 : 172.16.0.2 . 12345 }
}
map port_forwards_udp_ipv6 {
type ipv6_addr . inet_service : ipv6_addr . inet_service
# lets forward port for our torrent. Our 12345 to 12345 on fdf5:6028:947d:1234::2
elements = { $IP_WORLD_V6 . 12345 : [fdf5:6028:947d:1234::2] . 12345}
}
chain inbound_world {
# accepting ping (icmp-echo-request) for diagnostic purposes.
# However, it also lets probes discover this host is alive.
# This sample accepts them within a certain rate limit:
#
# icmp type echo-request limit rate 5/second accept
# Allow IPv6 configuration packets
icmpv6 type {nd-neighbor-solicit,nd-neighbor-advert,nd-router-solicit,
nd-router-advert,mld-listener-query,destination-unreachable,
packet-too-big,time-exceeded,parameter-problem} accept
# allow SSH
tcp dport { 22 } accept
# allow VPN connection
udp dport { $PORT_VPN } accept
}
chain inbound_vpn {
# accepting ping (icmp-echo-request) for diagnostic purposes.
icmp type echo-request limit rate 5/second accept
# Allow IPv6 configuration packets
icmpv6 type {nd-neighbor-solicit,nd-neighbor-advert,nd-router-solicit,
nd-router-advert,mld-listener-query,destination-unreachable,
packet-too-big,time-exceeded,parameter-problem} accept
# allow DNS and SSH from the private network
tcp dport { 22, 53 } accept
udp dport { 53 } accept
}
chain inbound {
# drop all traffic by default
type filter hook input priority filter; policy drop;
# Allow traffic from established and related packets, drop invalid
ct state vmap { established : accept, related : accept, invalid : drop }
# Allow dnat (port forwarding)
ct status dnat accept
# allow loopback traffic, anything else jump to chain for further evaluation
iifname vmap { lo : accept, $DEV_WORLD : jump inbound_world, $DEV_VPN : jump inbound_vpn}
# the rest is dropped by the above policy
}
chain forward {
type filter hook forward priority filter; policy drop;
# Allow traffic from established and related packets, drop invalid
ct state vmap { established : accept, related : accept, invalid : drop }
# Allow port forwarding
ct status dnat accept
# connections from the internal nets to the out nets are allowed
iifname $DEV_LOCAL_NETS oifname $DEV_OUT_NETS accept
# the rest is dropped by the above policy
}
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
dnat ip addr . port to ip daddr . tcp dport map @port_forwards_tcp_ipv4
dnat ip6 addr . port to ip6 daddr . tcp dport map @port_forwards_tcp_ipv6
dnat ip addr . port to ip daddr . udp dport map @port_forwards_udp_ipv4
dnat ip6 addr . port to ip6 daddr . udp dport map @port_forwards_udp_ipv6
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
# Hide IPs from local nets to the internet.
# We are using SNAT because we have static IP and it wil work faster than MASQUERADE
iifname $DEV_LOCAL_NETS oifname $DEV_WORLD snat ip to $IP_WORLD_V4
iifname $DEV_LOCAL_NETS oifname $DEV_WORLD snat ip6 to $IP_WORLD_V6
}
}
I added comments in the file for the rules explanation.
Enable and firewall
Load new rules to the firewall:
sudo nft -f /etc/nftables.conf
Enable firewall auto start:
sudo systemctl enable nftables
Test it
Connect from your client to the VPN server:
sudo wg-quick up wg0-client
And ping www.google.com:
ping -c 4 www.google.com
Check your DNS server. It should say 172.16.0.1
or fdf5:6028:947d:1234::1
responded.
nslookup www.google.com
Check that your IP addresses are changed, go to www.whatismyip.com.
If you want to disconnect from the VPN you can bring the VPN interface down.
sudo wg-quick down wg0-client
Done
That’s it. It should work.
If you want to add more clients:
- Generate new client private and public keys
- Create client config with private key and unused IPs from the pool
- Add client public key and these IPs as Peer to the server
Useful links that helped with this post:
- WireGuard official site: www.wireguard.com.
- Unbound configuration parameters: unbound.conf.html.
- Nftables Wiki: wiki.nftables.org.
Leave a Reply