Cookie Consent by TermsFeed

Make your own VPN - Wireguard, ipv6 and ad-blocking included

Make your own VPN - Wireguard, ipv6 and ad-blocking included

Note: This article assumes a setup based on OpenBSD. If you prefer a version based on FreeBSD, it is available here.

VPNs are a fundamental tool for securely connecting to your own servers and devices. Many people use commercial VPNs for various reasons, ranging from not trusting their provider (especially when connecting from a public hotspot) to wanting to “go out” on the Internet with a different IP address, perhaps from another country.

Whatever the reason, solutions are not lacking. I have always set up management VPNs to allow servers and/or clients to communicate with each other using secure channels. Lately, I have been activating IPv6 connectivity on all my devices (both desktop/servers and mobile devices) and I needed to quickly create a node that concentrated some networks and allowed them to go out on the network in IPv6. The tools I used and will describe are:

  • VPS - in this case, I used a basic Hetzner Cloud VPS (using this link, you will receive 20 euros of cloud credits), but any provider that provides IPv6 connectivity will do - if you want IPv6, of course.
  • OpenBSD - a clean, stable, and secure operating system.
  • Wireguard - lightweight, secure, and at the same time, not very “chatty”, so it is also gentle on mobile device batteries. When there is no traffic, it simply does not transmit/receive anything. Well supported by all major desktop and server operating systems as well as Android and iOS devices.
  • Unbound - can make DNS queries directly to root servers, not through forwarders. It also allows you to insert block-lists and have a result similar to that of Pi-Hole (i.e., ad-blocking).
  • SpamHaus lists - to immediately stop connections to and from users on blacklists.

The first step is to activate a VPS and install OpenBSD. On the Hetzner cloud console, there won’t be a pre-built OpenBSD image, but only a selection of Linux distributions. Don’t worry, just choose any of them and create the VPS. Once done, the OpenBSD ISO image will be available among the “ISO Images”. Just insert the virtual CD, restart the VPS, and the OpenBSD installation will appear in the console.

I won’t go into detail, the operation is simple and straightforward. The only precaution (in the case of a Hetzner Cloud VPS) is to use “autoconf” for IPv4 but, for now, do not configure IPv6. It will be configured later.

Install all OpenBSD updates (using the syspatch command) and restart, the kernel will be relinked.

Wireguard, on OpenBSD, is fully integrated into the base system and does not require the installation of external packages. This is a big advantage because over time, support for everything related to Wireguard will be managed directly by the main OpenBSD development team.

The first step is to configure IPv6 on the VPS. In the case of Hetzner, unfortunately, they only provide a /64, so it will be necessary to segment the assigned network. In this example, it will be divided into /72 subnetworks - to find valid subclasses, it will be possible to use a calculator.

The /etc/hostname.vio0 file should look something like this:

inet autoconf
inet6 2a01:4f8:cafe:cafe::1 72 
!route add -net ::/0 fe80::1%vio0

In short, keep the base address assigned by Hetzner, but change the netmask to /72 - thus giving the possibility of having other networks available.

sh /etc/netstart vio0

It will reconfigure the network interface and allow IPv6 to start working. To test it:


If everything has been configured correctly, the ping will be executed and will reply.

It is now necessary to enable forwarding for IPv4 and IPv6. Enter these lines in the /etc/sysctl.conf file:


To apply those changes you can reboot or just type:

sysctl net.inet.ip.forwarding=1
sysctl net.inet6.ip6.forwarding=1

To configure Wireguard, a few steps will be necessary. First of all, the private key will need to be created:

openssl rand -base64 32

Something like this will come out:


Now create a new file called /etc/hostname.wg0: wgport 51820 wgkey YUkS6cNTyPbXmtVf/23ppVW3gX2hZIBzlHtXNFRp80w=
inet6 2a01:4f8:cafe:cafe:100::1 72

A new Wireguard interface called wg0 is being created. It will have the IPv4 address “”, Wireguard will listen on port 51820, and with the private key created shortly before. It will also have an IPv6 address on one of the subclasses that the provider will have provided.

Save and activate the interface:

sh /etc/netstart wg0

If everything has been entered correctly, it should enable the interface. Now:

ifconfig wg0

And something like this will be returned:

	index 5 priority 0 llprio 3
	wgport 51820
	wgpubkey xxxxxxxxxxxxxxx=
	groups: wg
	inet netmask 0xffffff00 broadcast
	inet6 2a01:4f8:cafe:cafe:100::1 prefixlen 72

Take note of the wgpubkey - it will be needed to configure the clients.

As for the firewall, OpenBSD comes with a basic pf configuration. In my setups, I tend to block what is not needed and be permissive with what may be useful. However, I like to keep out the “bad guys,” so I use blacklists. pf allows elements to be inserted and removed from tables in runtime, so the firewall can be configured accordingly.

To download and apply Spamhaus lists, I use a simple but effective script found on the Internet.

So create the script in /usr/local/sbin/

# this is normally run once per day via /etc/daily.local.
echo updating Spamhaus DROP lists:
  { ftp -o - && \
    ftp -o - ; \
  } 2>/dev/null | sed "s/;/#/" > /var/db/drop.txt
pfctl -t spamhaus -T replace -f /var/db/drop.txt

Make it executable and run it:

chmod a+rx /usr/local/sbin/

There are many possibilities to configure pf. A fairly simple example could be this:

wg0_if = "wg0"
wg0_networks = ""

set skip on lo

# Spamhaus DROP list:
table <spamhaus> persist file "/var/db/drop.txt"

block drop log quick from <spamhaus>

match in all scrub (no-df random-id max-mss 1440)

match out on $ext_if from { $wg0_networks } nat-to ($ext_if)

#Pass ICMP on ipv6
pass quick proto ipv6-icmp
#Block from ipv6 to wg0 network
block in quick on $ext_if inet6 to { 2a01:4f8:cafe:cafe:100::/72 }
#Pass Wireguard traffic - in and out
pass quick on $wg0_if

# default deny
block in
block out

# By default, do not permit remote connections to X11
block return in on ! lo0 proto tcp to port 6000:6010

# Port build user does not need network
block return out log proto {tcp udp} user _pbuild

pass in on $ext_if proto tcp to port ssh
pass in on $ext_if proto udp to port 51820

pass out on $ext_if

This is a very simple configuration: it blocks everything that is present in the list downloaded from Spamhaus, allows NAT from the Wireguard network to the public interface, allows icmp traffic in IPv6 (necessary for the network to function properly) while blocking incoming traffic to the Wireguard IPv6 LAN (remember that the IPs will be public and directly reachable, so we don’t want to expose our devices by default). All traffic on the Wireguard interface will be allowed to pass. Then everything will be blocked and exceptions will be specified, i.e. allowing ssh and Wireguard connections (of course). Authorization will also be granted to allow traffic to exit from the public network interface.

Reload pf configuration:

pfctl -f /etc/pf.conf

If everything went correctly, the firewall should have loaded the new options.

To obtain caching of DNS queries and the related ad-block, it is now time to configure Unbound. A while ago, I found a script which I slightly adapted. I don’t remember where I got it, so I’ll paste it here without citing the original creator.

Create a script to update the unbound ad-block, in /usr/local/sbin/

# Using blacklist from pi-hole project
# to enable AD blocking in unbound(8)

# Available blocklists - comment line to disable blocklist

# Global variables
_tmpfile="$(mktemp)" && echo '' > $_tmpfile

# Remove comments from blocklist
function simpleParse {
  ftp -VMo - $1 | \
  sed -e 's/#.*$//' -e '/^[[:space:]]*$/d' >> $2

# Parse MalwareDom
#[[ -n ${_malwaredom+x} ]] && simpleParse $_malwaredom $_tmpfile

# Parse ZeusTracker
#[[ -n ${_zeustracker+x} ]] && simpleParse $_zeustracker $_tmpfile

# Parse DisconTrack
[[ -n ${_discontrack+x} ]] && simpleParse $_discontrack $_tmpfile

# Parse DisconAD
[[ -n ${_disconad+x} ]] &&  simpleParse $_disconad $_tmpfile

# Parse StevenBlack
[[ -n ${_stevenblack+x} ]] && \
  ftp -VMo - $_stevenblack | \
  sed -n '/Start/,$p' | \
  sed -e 's/#.*$//' -e '/^[[:space:]]*$/d' | \
  awk '/^ { print $2 }' >> $_tmpfile

# Parse hpHosts
[[ -n ${_hostfiles+x} ]] && \
  ftp -VMo - $_hostfiles | \
  sed -n '/START/,$p' | tr -d '^M$' | \
  sed -e 's/#.*$//' -e '/^[[:space:]]*$/d' -e 's/^M$//' | \
  awk '/^ { print $2 }' >> $_tmpfile

# Create unbound(8) local zone file
sort -fu $_tmpfile | grep -v "^[[:space:]]*$" | \
awk '{
  print "local-zone: \"" $1 "\" redirect"
  print "local-data: \"" $1 " A\""
}' > $_unboundconf && rm -f $_tmpfile

/usr/sbin/rcctl reload unbound 1>/dev/null

exit 0

Similarly, make the script executable and run it:

chmod a+rx /usr/local/sbin/

Now, the Unbound configuration file in /var/unbound/etc/unbound.conf can be modified as follows:

# $OpenBSD: unbound.conf,v 1.21 2020/10/28 11:35:58 sthen Exp $

        verbosity: 1
        log-queries: no
        num-threads: 4
        num-queries-per-thread: 1024
        #interface:      # listen on alternative port
	interface: 2a01:4f8:cafe:cafe:100::1
        interface: ::1
        outgoing-range: 64
        chroot: ""
        #do-ip6: yes

        # override the default "any" address to send queries; if multiple
        # addresses are available, they are used randomly to counter spoofing
        #outgoing-interface: 2001:db8::53

        access-control: refuse
        access-control: allow
        access-control: ::0/0 refuse
        access-control: ::1 allow
        access-control: allow
     	access-control: 2a01:4f8:cafe:cafe:100::/72 allow

        hide-identity: yes
        hide-version: yes

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

        # Synthesize NXDOMAINs from DNSSEC NSEC chains.
        aggressive-nsec: yes
        prefetch: yes
        username: "nobody"
        directory: "/var/unbound/etc"
        logfile: "/var/unbound/unbound.log"
        use-syslog: no
        pidfile: "/var/unbound/"
        include: /var/unbound/etc/unbound-adhosts.conf

        control-enable: yes
        control-interface: /var/run/unbound.sock

Before launching unbound, it is necessary to give the appropriate permissions:

chown -R nobody:nobody /var/unbound

Now it is possible to enable and start unbound. Since it needs to load the (long) blocklist, it will take a few seconds:

rcctl enable unbound
rcctl start unbound

If everything has been done correctly, unbound will be able to respond to requests made on and 2a01:4f8:cafe:cafe:100::1, from their respective LANs.

Now it is possible to configure the Wireguard client. Each implementation has its own procedure (Android, iOS, MikroTik, Linux, etc.) but essentially it is sufficient to create the right configuration both on the server and on the client. For example, the server’s public key (visible by typing “ifconfig wg0” on the OpenBSD server) should be inserted into the “peer” that will be created on the client, while the client’s public key will be used on the server in this way:

Reopen the file /etc/hostname.wg0 and add: wgport 51820 wgkey YUkS6cNTyPbXmtVf/23ppVW3gX2hZIBzlHtXNFRp80w=
inet6 2a01:4f8:cafe:cafe:100::1 72
wgpeer *client's public key* wgaip wgaip 2a01:4f8:cafe:cafe:100::2/128

Reload the configuration:

sh /etc/netstart wg0

On the client, create a new configuration by inserting “, 2a01:4f8:cafe:cafe:100::2/128” (the ones that were entered in the peer configuration in the hostname.wg0 file) in the local IP addresses. Set the DNS server address to “” and/or its corresponding IPv6 address (in the example, 2a01:4f8:cafe:cafe:100::1 - yours will be different). In the peer, insert the server’s data, including its public key, IP address:port (in the example, the port is 51820), and allowed addresses (setting “, ::0/0” means “all connections will be sent via Wireguard” - all the traffic will pass through the VPN for both IPv4 and IPv6).

It is also possible to use the VPN only as an ad-blocker, by only routing DNS traffic through it. To achieve this result, it is sufficient to configure the client so that the only allowed address is the one of the just-configured unbound (in this example, or 2a01:4f8:cafe:cafe:100::1) - DNS resolution will occur via VPN, but browsing will continue to work through the main provider.

If you want the spamhaus and ad-block lists to be updated automatically, create the /etc/daily.local file and add the following lines:



All of this can be achieved simply with a basic installation of OpenBSD, without the need to install any additional packages. This is an advantage both in terms of update management and security.

See also