HOW TO SET UP A WIREGUARD VPN SERVER WITH UNBOUND ON OPENBSD

2023-01-11
Some months ago, I published an article on how to set up a Wireguard server with adblocking capabilities on GNU/Linux systems, focusing Debian and PiHole specifically. Recently I wanted to reproduce the same setup on an OpenBSD server(since the Wireguard protocol is available on *BSD systems as well) and, while PiHole is not currently available for *BSD systems, I managed to accomplish the same result using the DNS resolver unbound(8) and unbound-adblock to fetch updated blocklists every day. In this guide, I will show you how to achieve the same result.

Installation

Let us get started by installing the wireguard-tools package
                
blowfish$ doas pkg_add wireguard-tools
                
            
and by enabling packets forwarding:
                
blowfish# echo "net.inet.ip.forwarding=1" >> /etc/sysctl.conf
blowfish# echo "net.inet6.ip6.forwarding=1" >> /etc/sysctl.conf
blowfish# sysctl net.inet.ip.forwarding=1
blowfish# sysctl net.inet6.ip6.forwarding=1
                
            

Configure Wireguard(Server)

Now we can proceed to generate the server key pair and the configuration file. We will move these files inside a reserved directory:
                
blowfish# mkdir -p /etc/wireguard
blowfish# cd /etc/wireguard/
blowfish# wg genkey | tee privkey | wg pubkey > pubkey
                
            
Now we can copy the content of the privkey file and create a new file called wg0.conf where:
  1. PrivateKey is equal to privkey
  2. ListenPort is the UDP Port where the Wireguard server will listen to.
The final file should have the following structure:
                
blowfish# cat wg0.conf
[Interface]
PrivateKey = +LoX/Rrh2VR6nFiExOweXR37HluHdOhjBiFu7jqK7mo=
ListenPort = 48965
                
            

Configure Wireguard(Client)

Wireguard can be installed in a wide spectrum of operating system, in this guide I will not cover the installation process; in order to install the wireguard client for your computer/table/phone, please refer to this page. After that, open up the configuration file and add the following content:
                
[Interface]
PrivateKey = ni16f/oyWn8G0rdsJ7YGyytjXvJSfaNzhzFSG5Bv4Gg= # <-- client private key
Address = 192.168.2.2/24
DNS = 192.168.2.1

[Peer]
PublicKey = 4wzgj/0u53Jiheq8DjwQ9GRnvnzv0qcsisKARdnrr1c= # <-- server public key
PresharedKey = PW21sz8kl+nY8WRNJEypkqWJGLARSX2A5KjbPfaEUp0= # <-- wg genpsk
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <SERVER_IP_ADDRESS>:48965
PersistentKeepalive = 15 
                
            
And trim the following fields according to your needs:
  1. PrivateKey: replace it with client's private key(you can generate a new keypair using wg genkey | tee privkey | wg pubkey > pubkey: command if you do not use a graphical client);
  2. PublicKey: replace with server's public key(i.e. /etc/wireguard/pubkey file on the VPN server);
  3. PresharedKey: you can generate a preshared key with wg genpsk (this field is optional);
  4. Endpoint: the IP address of your server with the Wireguard UDP port.

Back to the server

To complete the configuration, add a new [Peer] entry inside the server configuration file, each [Peer] block represent a unique device of the network. Open the /etc/wireguard/wg0.conf file and add the following entry at the end:
                
[Interface]
PrivateKey = +LoX/Rrh2VR6nFiExOweXR37HluHdOhjBiFu7jqK7mo=
ListenPort = 48965

# Add this
[Peer]
PublicKey = 1+54fGF/zZlVTxDiJ3rlmrH65+5K1NMFKwxlniA/2js= # <-- Client public key
PresharedKey = PW21sz8kl+nY8WRNJEypkqWJGLARSX2A5KjbPfaEUp0=
AllowedIPs = 192.168.2.2/32
                
            
Where:
  1. PublicKey: is the public key of the client;
  2. PresharedKey: is the preshared key previously generated on the client configuration;
  3. AllowedIPs: is the client's IP address.
This means that every time you want to add a new client to the network, you simply create a new keypair, add the public key to the server configuration file and restart the Wireguard network interface. Just be sure to assign a unique IP address to each client.

Configure network interface

Now we can start to configure the Wireguard network interface. In OpenBSD this is done by creating a new file called /etc/hostname.wg0 containing the gateway address(i.e., the address of the interface) and the netmask. In our example, the interface address is 192.168.2.1 while the network mask is 255.255.255.0 which gives us \( 2^h - 2 = 2^{8} - 2 = \boxed{254} \) valid IP addresses.

The complete file should look like this:
                
blowfish# cat /etc/hostname.wg0
inet 192.168.2.1 255.255.255.0 NONE
up

!/usr/local/bin/wg setconf wg0 /etc/wireguard/wg0.conf
                
            
Be sure to trim the last line with the correct Wireguard configuration file if you have chosen a different path than /etc/wireguard/wg0.conf.

Configure the firewall

Finally, we need to configure the firewall(pf.conf(5)). The only three rules needed by Wireguard are the following: To do so, add the following rules to /etc/pf.conf:
                
blowfish# cat /etc/pf.conf
[...]
# Allow inbound traffic on Wireguard interface
pass in on wg0
# Allow all UDP traffic on Wireguard port
pass in inet proto udp from any to any port 56709
# Set up a NAT for Wireguard
pass out on egress inet from (wg0:network) nat-to (vio0:0)
                
            

Start Wireguard and reload PF

Now we can start the Wireguard interface and reload the firewall rules by issuing the following commands:
                
blowfish# sh /etc/netstart wg0
WARNING: /etc/hostname.wg0 is insecure, fixing permissions.
blowfish# pfctl -f /etc/pf.conf 
                
            
You should be able to see the new client with the wg(9) utility:
                
blowfish$ doas wg
interface: wg0
    public key: 4wzgj/0u53Jiheq8DjwQ9GRnvnzv0qcsisKARdnrr1c=
    private key: (hidden)
    listening port: 48965

peer: 1+54fGF/zZlVTxDiJ3rlmrH65+5K1NMFKwxlniA/2js=
    preshared key: (hidden)
    allowed ips: 192.168.2.2/32
                
            
You can also enable the VPN connection from the client side. Keep in mind that right now you will not be able to access the internet. This is normal because the DNS server we have specified in the client's configuration file(i.e., 192.168.2.1) is not yet active. We will fix this in a moment by setting up unbound and unbound-adblock.

Configure Unbound

unbound(8) is a validating, recursive and caching DNS server used by *BSD systems to provide a simple yet modern name server. In this tutorial we will use unbound as DNS server with adblocking capabilities.

The first thing to do to configure unbound is to edit the /var/unbound/etc/unbound.conf file to make it listen on our Wireguard address(i.e., 192.168.2.1). The relevant parts of the configuration file are flagged by a comment, below there is the complete file:
                
blowfish# cat /var/unbound/etc/unbound.conf
# Unbound Configuration
# By Marco Cetica 2023
#

server:
	interface: 127.0.0.1
	interface: 192.168.2.1    # <-- VPN address
	interface: ::1

	access-control: 0.0.0.0/0 refuse
	access-control: 127.0.0.0/8 allow
	access-control: 192.168.2.0/24 allow # <-- VPN IPs range
	access-control: ::0/0 refuse
	access-control: ::1 allow

	hide-identity: yes
	hide-version: yes
	port: 53 	# <-- Specify listening port

	# Security options
	hide-identity: yes  # <----- Mask version and identity
	hide-version: yes   # <--/
	private-address: 192.168.0.0/16 # <-- Avoid returning private addresses

	# Perform DNSSEC validation.
	#
	auto-trust-anchor-file: "/var/unbound/db/root.key"
	val-log-level: 2

	# Synthesize NXDOMAINs from DNSSEC NSEC chains.
	# https://tools.ietf.org/html/rfc8198
	#
	aggressive-nsec: yes


remote-control:
	control-enable: yes
	control-interface: /var/run/unbound.sock
                
            
After that we can enable and start the unbound service by issuing:
                
blowfish# rcctl enable unbound
blowfish# rcctl start unbound  
unbound(ok)
                
            
Then, we can check whether the daemon is listening on the Wireguard address with the following command:
                
blowfish# netstat -na -f inet | grep "192.168.2" 
tcp          0      0  192.168.2.1.53         *.*                    LISTEN
udp          0      0  192.168.2.1.53         *.*  
                
            
It is working!

Configure unbound-adblock

The last step of this guide is to configure unbound-adblock. unbound-adblock is a POSIX compliant script to fetch an updated blocklist of advertising, analytics and tracking domains. At the time of writing the latest version of unbound-adblock is v0.5; the installing instructions provided below will reflect this constraint. Check out the official website for updated instructions.
  1. Download the script:
                            
    blowfish$ ftp https://geoghegan.ca/pub/unbound-adblock/0.5/unbound-adblock.sh
                            
                        
  2. Add a new user for unbound-adblock:
                            
    blowfish$ doas useradd -s /sbin/nologin -d /var/empty _adblock
                            
                        
  3. Install the script with appropriate permissions:
                            
    blowfish$ doas install -m 755 -o root -g bin unbound-adblock.sh /usr/local/bin/unbound-adblock
                            
                        
  4. Install optional dependencies:
                            
    blowfish$ doas pkg_add ripgrep mawk
                            
                        
  5. Create required files:
                            
    blowfish$ doas install -m 644 -o _adblock -g wheel /dev/null /var/unbound/db/adblock.rpz
    blowfish$ doas install -d -o root -g wheel -m 755 /var/log/unbound-adblock
    blowfish$ doas install -o _adblock -g wheel -m 640 /dev/null /var/log/unbound-adblock/unbound-adblock.log
    blowfish$ doas install -o _adblock -g wheel -m 640 /dev/null /var/log/unbound-adblock/unbound-adblock.log.0.gz
                            
                        
  6. Configure /etc/doas.conf to allow _adblock user to execute the unbound-adblock script:
                            
    blowfish$ cat /etc/doas.conf
    # Rules for unbound-adblock
    permit root
    permit nopass _adblock cmd /usr/sbin/unbound-control args -q status
    permit nopass _adblock cmd /usr/sbin/unbound-control args -q flush_zone unbound-adblock
    permit nopass _adblock cmd /usr/sbin/unbound-control args -q auth_zone_reload unbound-adblock
                            
                        
  7. Configure unbound-control:
                            
    blowfish$ doas unbound-control-setup
                            
                        

Configure Unbound(again)

Now let us add the blocklist into unbound configuration file. To do so, add the following entry before the remote-control part:
                
# Required modules for RPZ
module-config: "respip validator iterator"
	rpz:
	   name: "unbound-adblock"
	   zonefile: "/var/unbound/db/adblock.rpz"
	   rpz-log: yes
	   rpz-log-name: "unbound-adblock"
                
            
The complete configuration file should look like this:
                
blowfish$ doas cat /var/unbound/etc/unbound.conf
# Unbound Configuration
# By Marco Cetica 2023
#

server:
	interface: 127.0.0.1
	interface: 192.168.2.1    # <-- VPN address
	interface: ::1

	access-control: 0.0.0.0/0 refuse
	access-control: 127.0.0.0/8 allow
	access-control: 192.168.2.0/24 allow # <-- VPN IPs range
	access-control: ::0/0 refuse
	access-control: ::1 allow

	hide-identity: yes
	hide-version: yes
	port: 53 	# <-- Specify listening port

	# Security options
	hide-identity: yes  # <----- Mask version and identity
	hide-version: yes   # <--/
	private-address: 192.168.0.0/16 # <-- Avoid returning private addresses

	# Perform DNSSEC validation.
	#
	auto-trust-anchor-file: "/var/unbound/db/root.key"
	val-log-level: 2

	# Synthesize NXDOMAINs from DNSSEC NSEC chains.
	# https://tools.ietf.org/html/rfc8198
	#
	aggressive-nsec: yes

# Required modules for RPZ
module-config: "respip validator iterator"
	rpz:
	   name: "unbound-adblock"
	   zonefile: "/var/unbound/db/adblock.rpz"
	   rpz-log: yes
	   rpz-log-name: "unbound-adblock"

remote-control:
	control-enable: yes
	control-interface: /var/run/unbound.sock
                
            
Finally, restart unbound:
                
blowfish$ doas rcctl restart unbound
unbound(ok)
unbound(ok)

                
            
and execute the unbound-adblock script for the first time:
                
blowfish$ doas -u _adblock unbound-adblock -O openbsd
unbound-checkconf: no errors in /var/unbound/etc/unbound.conf

unbound-adblock: 
Changes (+/-):  +207047
Domain total :  207047
                
            
You can also run this script automatically every night with the following crontab rule:
                
blowfish$ doas crontab -u _adblock -e 
~ 0~1 * * *	-s unbound-adblock -O openbsd

                
            
Be sure to add a blank line at the end of the crontab file.

Conclusions

At this point you should be able to reach the internet. Let us try our new VPN on some sites(apart from whatismyipaddress.com):

blockads.fivefilters.org

This page checks whether our ads blocking system works(you can also try to open any newspaper website with adblock disabled…those websites are the perfect testing page for an anti-ads system): adblock testing

dnsleaktest.com

This website checks whether your VPN/DNS leaks your real IP address. Be sure to restart your network daemon(i.e., NetworkManager on Linux) before starting this test to avoid false-positive results. dnsleak test

dnssec-tools.org

This page determines whether your DNS resolver validates DNSSEC signatures. dnssec test