Running dnscrypt-proxy on OpenBSD

Mar 12, 2021 [ #openbsd #raspberrypi #hack #dns #100daystooffload ]

Contents

Intro

A couple of weeks ago I took a spare RaspberryPi 3 leftover from my old k3s cluster and installed OpenBSD on it using my Pocket C.H.I.P.. While getting it installed was fun, I wanted to do more with it and use it on a more regular basis to continue learning about OpenBSD in general.

OpenBSD on RaspberryPi

OpenBSD 4.9 Sticker on RaspberryPi 3

I’ve had a Pi-hole running on an older Raspberry Pi B with Debian for a few years, but wanted a few additional features, notably using DNSCRYPT to encrypt DNS traffic so our ISP wouldn’t be able to use it for anything and/or using DNS-over-HTTPS. I originally was going to setup Pi-hole on the new OpenBSD Pi, but quickly found out that Pi-hole doesn’t work on OpenBSD.

A quick search, turned up an excellent Pi-hole on OpenBSD guide, which cleverly uses the vmm hypervisor to run a Linux VM and install Pi-hole there. This however was also a dead-end since the guide was assuming an x86 install and not arm64. Unfortunately it seems that the OpenBSD arm64 port doesn’t have vmm so installing a VM wouldn’t work, and probably wasn’t a great idea for performance anyway.

The guide did include a reference to using dnscrypt-proxy, which is available as a package for OpenBSD arm64. Reading through the features it can do almost everything Pi-hole can and more,

The only thing that was missing was the nicer GUI interface of Pi-hole, but I rarely used that anyway after initially setting it up and was more eye-candy that utilitarian. I decided to setup dnscrypt-proxy to mimic the blocking capabilities of the Pi-hole and enabled some more of the advanced features.

Installing dnscrypt-proxy on OpenBSD

Because dnscrypt-proxy is in the OpenBSD package repo, installation was a simple as,

$ doas pkg_add dnscrypt-proxy
quirks-3.442 signed on 2021-03-09T20:09:44Z
dnscrypt-proxy-2.0.44: ok

This installs version 2.0.44 which is slightly older than upstream, which is 2.0.45. Looking in openbsd snapshots, 2.0.45 is packaged in preparation for OpenBSD 6.9 and can install without issue on 6.8.

$ wget https://cdn.openbsd.org/pub/OpenBSD/snapshots/packages/aarch64/dnscrypt-proxy-2.0.45.tgz
$ doas pkg_add dnscrypt-proxy-2.0.45.tgz

Configuring dnscrypt-proxy on OpenBSD

The default configuration is in /etc/dnscrypt-proxy.toml, and will need to be updated before starting dnscrypt-proxy since it will give one of these errors,

[FATAL] Unable to clone file descriptor: [bad file descriptor]

or

[FATAL] Duplicated file descriptors are above base

It also contains deprecated references to blacklist and whitelist which will needs replacing. A known good configuration to start with is here: https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml.

Cloudflare DNS-over-HTTPS

I am using the Cloudflare 1.1.1.1 DNS revolvers since they provide DNS-over-HTTPS and their response times are extremely fast. My ISP and network is also setup for IPv6, and that is configured to allow IPv6 clients and lookups. For fallback DNS, Quad9 is used since it’s separate from Cloudflare and has a relatively decent privacy and security features.

#Use cloudflare DNS
server_names = ['cloudflare', 'cloudflare-ipv6']

#Listen on local and LAN addresses for DNS
listen_addresses = ['127.0.0.1:53', '[::1]:53', '192.168.7.221:53', '[fd82:738a:110d:1:2259:a6b:cd78:733b]:53']
max_clients = 250
user_name = '_dnscrypt-proxy'

#Enable ipv4 and ipv6
ipv4_servers = true
ipv6_servers = true

#Include resolvers with the following configuration
dnscrypt_servers = true
doh_servers = true
require_dnssec = true
require_nolog = true
require_nofilter = true

#Allow TCP and UDP
force_tcp = false
timeout = 2500
keepalive = 30

#Logging
log_level = 2
use_syslog = true

#Certs
cert_refresh_delay = 240
dnscrypt_ephemeral_keys = true
tls_disable_session_tickets = true

#Fallback to a non CloudFlare DNS if things aren't happy
fallback_resolver = '9.9.9.9:53'
ignore_system_dns = false

Sources

To reference revolvers and relays, sources are used to find publicly resources. These are setup by default in dnscrypt-proxy, but I’ve added a few changes like caching them to /var/dnscrypt-proxy and point to the latest v3.

#Sources for resolvers and relays
[sources]
  [sources.'public-resolvers']
  urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md']
  minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
  cache_file = '/var/dnscrypt-proxy/public-resolvers.md'
  refresh_delay = 72

  [sources.'relays']
  urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://ipv6.download.dnscr
ypt.info/resolvers-list/v3/relays.md', 'https://download.dnscrypt.net/resolvers-list/v3/relays.md']
  minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
  cache_file = '/var/dnscrypt-proxy/relays.md'
  refresh_delay = 72
  prefix = ''

Block and Allow Lists

Another powerful features of dnscrypt-proxy are filters, which can take on the role Pi-hole was doing with having a list of domains to block for ads, malware, and other reasons. To generate these lists, the generate-domains-blocklist.py is used.

I took the blocklists that Pi-hole was using, and created a domains-blocklist.conf configuration to match, which gives it the same blocking sources as the Pi-hole was.

# Local additions
file:domains-blocklist-local-additions.txt

# Peter Lowe's Ad and tracking server list
https://pgl.yoyo.org/adservers/serverlist.php?hostformat=nohtml

# Ads filter list by Disconnect
https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt

# Basic tracking list by Disconnect
https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt

# Sysctl list (ads)
http://sysctl.org/cameleon/hosts

# BarbBlock list (spurious and invalid DMCA takedowns)
https://paulgb.github.io/BarbBlock/blacklists/domain-list.txt

# NoTracking's list - blocking ads, trackers and other online garbage
https://raw.githubusercontent.com/notracking/hosts-blocklists/master/dnscrypt-proxy/dnscrypt-proxy.blacklist.txt

# Geoffrey Frogeye's block list of first-party trackers - https://hostfiles.frogeye.fr/
https://hostfiles.frogeye.fr/firstparty-trackers.txt

# Steven Black hosts file
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts

# Pihole Lists
https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist
https://raw.githubusercontent.com/nickspaargaren/pihole-google/master/categories/androidparsed
https://raw.githubusercontent.com/nickspaargaren/pihole-google/master/categories/analyticsparsed
https://raw.githubusercontent.com/anudeepND/blacklist/master/facebook.txt

This file also references domains-blocklist-local-additions.txt, which is setup to protect against DNS rebinding protection,

# Localhost rebinding protection
0.0.0.0
127.0.0.*

# RFC1918 rebinding protection
10.*
172.16.*
172.17.*
172.18.*
172.19.*
172.20.*
172.21.*
172.22.*
172.23.*
172.24.*
172.25.*
172.26.*
172.27.*
172.28.*
172.29.*
172.30.*
172.31.*
192.168.*

The generate-domains-blocklist.py script will also require the files domains-allowlist.txt and domains-time-restricted.txt, which I just created as empty files to allow the blocklist creation to proceed

$ touch domains-time-restricted.txt
$ touch domains-allowlist.txt

Generating the blocklist can be done manually or as a cronjob,

$ python3 generate-domains-blocklist.py -o blocklist.txt

Since I use Plex, work for a Account Based Marketing company, and Google reCaptcha is usually in blocklists, I created an allowlist.txt as well,

plex.direct
demandbase.com
recaptcha.google.com
gc.zgo.a

Putting this all together into the /etc/dnscrypt-proxy.toml blocking configuration,

#Blocking configuration
[blocked_names]
  ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file)
  blocked_names_file = '/home/micheal/dnscrypt-proxy/blocklist.txt'

  log_file = '/var/tmp/blocked.log'
  log_format = 'tsv'

#Allow configuration
[allowed_names]
  ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file)
  allowed_names_file = '/home/micheal/dnscrypt-proxy/allowlist.txt'

Anonymized DNS

One of the features I liked about dnscrypt-proxy is it offers is Anonymized DNS, which I setup to test out.

#Anonymized DNS relays
[anonymized_dns]
routes = [
    { server_name='zackptg5-us-il-ipv4', via=['anon-cs-usca', 'anon-cs-usga'] },
    { server_name='freetsa.org-ipv6', via=['anon-zackptg5-us-il-ipv6', 'anon-acsacsar-ams-ipv6'] }
]

While this did work well, unfortunately the DNS query times were consistently 200ms+, which can appear as lag when browsing things. Since DNS-over-HTTPS is encrypted and filtering is setup, this was more of a nice-to-have instead of a requirements, so I ended up commenting it out. If/when Cloudflare provides DNSCRYPT I may re-visit it and see if response times have improved.

Local DNS-over-HTTPS

Another feature that Pi-hole didn’t support was DNS-over-HTTPS both for resolving and for serving requests locally. This is something built-in to dnscrypt-proxy with Local DoH that Firefox support for DoH can then use. By default Firefox will use Cloudflare DoH directly and was previously bypassing the Pi-hole, not getting the filtering features the rest of the network was. Now it can use the same filtering and continue to use DoH.

To setup DoH on dnscrypt-proxy, a self-signed certificate is required,

openssl req -x509 -nodes -newkey rsa:2048 -days 5000 -sha256 -keyout localhost.pem -out localhost.pem

This certificate is then used to listen on IPv4 and IPv6 addresses for DoH on port 3000,

#DNS over HTTPS configuration
[local_doh]
  listen_addresses = ['127.0.0.1:3000', '[::1]:3000', '192.168.7.221:3000', '[fd82:738a:110d:1:2259:a6b:cd78:733b]:3000']
  path = "/dns-query"
  cert_file = "/home/micheal/dnscrypt-proxy/localhost.pem"
  cert_key_file = "/home/micheal/dnscrypt-proxy/localhost.pem"

Firefox is then configured to use dnscrypt-proxy, for example over IPv6, https://fd82:738a:110d:1:2259:a6b:cd78:733b:3000/dns-query.

Enabling dnscrypt-proxy

Now that the configuration is all setup and dnscrypt-proxy is installed on OpenBSD, enable the service and start it,

$ doas rcctl enable dnscrypt_proxy
$ doas rcctl start dnscrypt_proxy

This should start and /var/log/messages will show it starting,

Mar 12 11:15:59 majora dnscrypt-proxy[1888]: dnscrypt-proxy 2.0.45
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Network connectivity detected
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Dropping privileges
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Network connectivity detected
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to 127.0.0.1:53 [UDP]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to 127.0.0.1:53 [TCP]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to [::1]:53 [UDP]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to [::1]:53 [TCP]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to 192.168.7.221:53 [UDP]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to 192.168.7.221:53 [TCP]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to [fd82:738a:110d:1:2259:a6b:cd78:733b]:53 [UDP]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to [fd82:738a:110d:1:2259:a6b:cd78:733b]:53 [TCP]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to https://127.0.0.1:3000/dns-query [DoH]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to https://[::1]:3000/dns-query [DoH]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to https://192.168.7.221:3000/dns-query [DoH]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Now listening to https://[fd82:738a:110d:1:2259:a6b:cd78:733b]:3000/dns-query [DoH]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Source [public-resolvers] loaded
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Source [relays] loaded
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Loading the set of whitelisting rules from [/home/micheal/dnscrypt-proxy/allowlist.txt]
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Firefox workaround initialized
Mar 12 11:15:59 majora dnscrypt-proxy[1888]: Loading the set of blocking rules from [/home/micheal/dnscrypt-proxy/blocklist.txt]
Mar 12 11:16:04 majora dnscrypt-proxy[1888]: [cloudflare-ipv6] OK (DoH) - rtt: 14ms
Mar 12 11:16:04 majora dnscrypt-proxy[1888]: [cloudflare] OK (DoH) - rtt: 83ms
Mar 12 11:16:04 majora dnscrypt-proxy[1888]: Sorted latencies:
Mar 12 11:16:04 majora dnscrypt-proxy[1888]: -    14ms cloudflare-ipv6
Mar 12 11:16:04 majora dnscrypt-proxy[1888]: -    83ms cloudflare
Mar 12 11:16:04 majora dnscrypt-proxy[1888]: Server with the lowest initial latency: cloudflare-ipv6 (rtt: 14ms)

Logging

Initially to test things are working well, setup the query logs to write to /var/tmp/query.log,

#Query logging, commented out unless for troubleshooting
[query_log]
  file = '/var/tmp/query.log'
  format = 'tsv'

As requests come in they will show up here. Blocked requests will also show up in /var/tmp/blocked.log.

Depending on the number of devices making DNS requests, the query.log can get quite large it’s recommended to keep it enabled when initially testing something, and turning it off when not in use. Leaving blocked.log on is a good idea to help know what to add to a allowlist.txt in case something is blocked that you want to allow.

Since a Raspberry Pi is most likely using an MicroSD card and by default OpenBSD will mount /tmp as a filesystem, it’s a good idea to to set /tmp as a memory filesystem to avoid excessive writes to the SD Card.

OpenBSD has the mfs filesytem that can be used to mount a filesystem in-memory to help avoid this,

mount_mfs is used to build a file system in virtual memory and then mount it on a specified node.

Setup /tmp as mfs, first by unmounting it. This may require killing sndio processes and using the console instead of ssh as it will give a resource busy error when trying to unmount /tmp.

$ doas umount /tmp
$ chmod 1777 /tmp

In /etc/fstab, comment out the old /tmp mount and add the mfs mount,

#1400ced5c75f17ee.d /tmp ffs rw,nodev,nosuid 1 2
swap /tmp mfs rw,nodev,nosuid,-s=256M 0 0

Reboot, and /tmp will now show up as a mfs type,

$ mount | grep /tmp
mfs:73353 on /tmp type mfs (asynchronous, local, nodev, nosuid, size=524288 512-blocks)

Full Configuration

Here is the full configuration of /etc/dnscrypt-proxy combined from all the snippets above,

#Use cloudflare DNS
server_names = ['cloudflare', 'cloudflare-ipv6']

#Listen on local and LAN addresses for DNS
listen_addresses = ['127.0.0.1:53', '[::1]:53', '192.168.7.221:53', '[fd82:738a:110d:1:2259:a6b:cd78:733b]:53']
max_clients = 250
user_name = '_dnscrypt-proxy'

#Enable ipv4 and ipv6
ipv4_servers = true
ipv6_servers = true

#Include resolvers with the following configuration
dnscrypt_servers = true
doh_servers = true
require_dnssec = true
require_nolog = true
require_nofilter = true

#Allow TCP and UDP
force_tcp = false
timeout = 2500
keepalive = 30

#Logging
log_level = 2
use_syslog = true

#Certs
cert_refresh_delay = 240
dnscrypt_ephemeral_keys = true
tls_disable_session_tickets = true

#Fallback to a non CloudFlare DNS if things arne't happy
fallback_resolver = '9.9.9.9:53'
ignore_system_dns = false

#[query_log]
#  file = '/var/tmp/query.log'
#  format = 'tsv'

#Sources for resolvers and relays
[sources]
  [sources.'public-resolvers']
  urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md']
  minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
  cache_file = '/var/dnscrypt-proxy/public-resolvers.md'
  refresh_delay = 72

  [sources.'relays']
  urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://download.dnscrypt.net/resolvers-list/v3/relays.md']
  minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
  cache_file = '/var/dnscrypt-proxy/relays.md'
  refresh_delay = 72
  prefix = ''

#Blocking configuration
[blocked_names]
  ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file)
  blocked_names_file = '/home/micheal/dnscrypt-proxy/blocklist.txt'

  log_file = '/var/tmp/blocked.log'
  log_format = 'tsv'

#Allow configuration
[allowed_names]
  ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file)
  allowed_names_file = '/home/micheal/dnscrypt-proxy/allowlist.txt'

#Anonymized DNS relays
#[anonymized_dns]
#routes = [
#    { server_name='zackptg5-us-il-ipv4', via=['anon-cs-usca', 'anon-cs-usga'] },
#    { server_name='freetsa.org-ipv6', via=['anon-zackptg5-us-il-ipv6', 'anon-acsacsar-ams-ipv6'] }
#]

#DNS over HTTPS configuration
[local_doh]
  listen_addresses = ['127.0.0.1:3000', '[::1]:3000', '192.168.7.221:3000', '[fd82:738a:110d:1:2259:a6b:cd78:733b]:3000']
  path = "/dns-query"
  cert_file = "/home/micheal/dnscrypt-proxy/localhost.pem"
  cert_key_file = "/home/micheal/dnscrypt-proxy/localhost.pem"