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 thewireguard-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:
PrivateKey
is equal toprivkey
ListenPort
is the UDP Port where the Wireguard server will listen to.
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:
-
PrivateKey
: replace it with client's private key(you can generate a new keypair usingwg genkey | tee privkey | wg pubkey > pubkey
: command if you do not use a graphical client); -
PublicKey
: replace with server's public key(i.e./etc/wireguard/pubkey
file on the VPN server); -
PresharedKey
: you can generate a preshared key withwg genpsk
(this field is optional); -
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:
-
PublicKey
: is the public key of the client; -
PresharedKey
: is the preshared key previously generated on the client configuration; -
AllowedIPs
: is the client's IP address.
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:
- Allow traffic over the Wireguard interface;
- Allow inbound UDP traffic on the selected port(in our example
48965
); -
Setup a NAT between the internal Wireguard interface -
wg0
- and the server external interface, in my casevio0
.
/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.
-
Download the script:
blowfish$ ftp https://geoghegan.ca/pub/unbound-adblock/0.5/unbound-adblock.sh
-
Add a new user for unbound-adblock:
blowfish$ doas useradd -s /sbin/nologin -d /var/empty _adblock
-
Install the script with appropriate permissions:
blowfish$ doas install -m 755 -o root -g bin unbound-adblock.sh /usr/local/bin/unbound-adblock
-
Install optional dependencies:
blowfish$ doas pkg_add ripgrep mawk
-
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
-
Configure
/etc/doas.conf
to allow_adblock
user to execute theunbound-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
-
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.