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-toolsGenerate 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_keyCreate 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 = 51820Where 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.serviceThe 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 0WireGuard 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-toolsGenerate 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_keyCreate 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 = 21Similar 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/128Connect to the server
Run this command to enable a VPN connection:
sudo wg-quick up wg0-clientCheck 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.1DNS 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 unboundDownload the root DNS servers list and save it.
sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.cacheConfiguration
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: yesStart it
We should enable the unbound service to autostart and start the DNS server.
sudo systemctl enable unbound
sudo systemctl start unboundTest it
Let’s check that it responds properly. Should return IP addresses of www.google.com.
nslookup www.google.com 172.16.0.1If 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 nftablesEnable 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=1Then reload the config without reboot:
sudo sysctl -pConfigure 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.confEnable firewall auto start:
sudo systemctl enable nftablesTest it
Connect from your client to the VPN server:
sudo wg-quick up wg0-clientAnd ping www.google.com:
ping -c 4 www.google.comCheck your DNS server. It should say 172.16.0.1 or fdf5:6028:947d:1234::1 responded.
nslookup www.google.comCheck 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-clientDone
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