___________________________________________
title: DNS Guardrails with dnscrypt-proxy
tags: linux dns containers docker networking tailscale
date: 2024-01-23
___________________________________________
Intro
Over the holidays we got our two younger children HP laptops for them
to do their school work on and to have a proper computer. While the
schools Google Classroom login effectively adds restrictions to
Chrome, I still wanted to have some guardrails on their Internet
access as well as ad-blocking.
The first thing I did was replace the Windows S install that came on
the laptops with Linux Mint as I’ve always enjoyed the Cinnamon
desktop environment and it has a low enough learning curve that the
kids could easily pick it up. After installing a few apps and games
(OpenRCT2) from the Software Center and setting their own passwords,
they were up and running and surfing the world-wide-web.
[Linux Mint]:
https://www.linuxmint.com/
[Cinnamon desktop environment]:
https://projects.linuxmint.com/cinnamon/
[OpenRCT2]:
https://openrct2.org/
Finally I added Tailscale to both laptops to put them on my tailnet.
This has benefits of accessing tailnet-only services, easier remote
access, and leveraging the dnscrypt-proxy on OpenBSD I setup a few
years ago for DNS.
[Tailscale]:
https://tailscale.com/
[tailnet]:
https://tailscale.com/kb/1136/tailnet
[dnscrypt-proxy on OpenBSD]:
https://www.ecliptik.com/Running-dnscrypt-proxy-on-OpenBSD/
Guardrails
My original DNS config worked well, but I wanted to add some
guardrails specifically for the kids laptops,
1. Cloudflare for Families
2. Ad Blocking
3. YouTube Restricted Mode via Cloaking
4. Accessible only from the Tailscale
[Cloudflare for Families]:
https://blog.cloudflare.com/introducing-1-1-1-1-for-families/
[Ad Blocking]:
https://github.com/DNSCrypt/dnscrypt-proxy/wiki/Public-blocklist
[YouTube Restricted Mode]:
https://support.google.com/a/answer/6212415
First I tried using the existing dnscrypt-proxy to provide a different
set of DNS resolvers depending on the source IP, but this wasn’t
possible. Eventually I came up with a seperate DNS infrastructure in a
container for the laptops to use,
[dnscrypt-proxy]:
https://github.com/DNSCrypt/dnscrypt-proxy
Container Stack
Dockerfile is used for building a container including dnscrypt-proxy,
FROM debian:trixie-slim
ENV DEBIAN_FRONTEND noninteractive
RUN apt update && \
apt install -y dnscrypt-proxy \
ca-certificates \
&& apt clean
WORKDIR /tmp
ENTRYPOINT [ "/usr/sbin/dnscrypt-proxy" ]
CMD [ "-config", "/etc/dnscrypt-proxy/dnscrypt-proxy.toml" ]
docker compose is used to bring up the stack, which includes a
tailscale container to provide network and access to other devices on
the tailnet. Configuration files are monted read-only from the current
directly and some volumes to maintain state across restarts.
[docker compose]:
https://docs.docker.com/compose/
docker-compose.yml
version: '3.9'
services:
tailscale:
container_name: tailscale-dnscrypt
hostname: dnscrypt-proxy
image: ghcr.io/tailscale/tailscale
stdin_open: true
environment:
- TS_AUTH_KEY=${TS_AUTH_KEY}
- TS_USERSPACE=true
- TS_STATE_DIR=/var/lib/tailscale
- TS_SOCKET=/var/run/tailscale/tailscaled.sock
volumes:
- dnscryptvarlib:/var/lib
restart: unless-stopped
dnscrypt-proxy:
build: .
stdin_open: true
volumes:
- ./dnscrypt-proxy.toml:/etc/dnscrypt-proxy/dnscrypt-proxy.toml:ro
- ./blocklist.txt:/etc/dnscrypt-proxy/blocklist.txt:ro
- ./cloaking-rules.txt:/etc/dnscrypt-proxy/cloaking-rules.txt:ro
- ./domains-allowlist.txt:/etc/dnscrypt-proxy/domains-allowlist.txt:ro
- keys:/etc/dnscrypt-proxy/keys
restart: unless-stopped
network_mode: 'service:tailscale'
volumes:
dnscryptvarlib:
keys:
dnscrypt-proxy
The dnscrypt-proxy configuration uses the cloudflare-family source and
ad-blocking using a blocklist.txt generated with
generate-domains-blocklist.py. All logs go to /dev/stdout so they
appear in docker compose logs.
[generate-domains-blocklist.py]:
https://github.com/DNSCrypt/dnscrypt-proxy/wiki/Combining-Blocklists
dnscrypt-proxy.toml
#Use cloudflare DNS
server_names = ['cloudflare-family']
#Listen on local and LAN addresses for DNS
listen_addresses = ['127.0.0.1:53']
max_clients = 250
user_name = '_dnscrypt-proxy'
#Enable ipv4 and ipv6
ipv4_servers = true
ipv6_servers = false
#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
#Cache
cache = true
#Cloaking
cloaking_rules = '/etc/dnscrypt-proxy/cloaking-rules.txt'
#Query logging, commented out unless for troubleshooting
[query_log]
file = '/dev/stdout'
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/tmp/public-resolvers.md'
refresh_delay = 72
#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 = '/etc/dnscrypt-proxy/blocklist.txt'
log_file = '/dev/stdout'
log_format = 'tsv'
#Allow configuration
[allowed_names]
allowed_names_file = '/etc/dnscrypt-proxy/domains-allowlist.txt
Cloaking is used so all YouTube requests resolve to
restrict.youtube.com.
[Cloaking]:
https://github.com/DNSCrypt/dnscrypt-proxy/wiki/Public-blocklist
cloaking-rules.txt
www.youtube.com restrict.youtube.com
m.youtube.com restrict.youtube.com
youtubei.googleapis.com restrict.youtube.com
youtube.googleapis.com restrict.youtube.com
www.youtube-nocookie.com restrict.youtube.com
Exposing via Tailscale
Since I only want DNS available to devices on my tailnet, and not
publicly available, there’s a tailscale container in the
docker-compose.yml that provides networking to the dnscrypt-proxy
container using network_mode.
Set this up by creating an auth key for your tailnet and then putting
it into a .env file that docker compose will source in and set as the
TS_AUTH_KEY variable.
[auth key]:
https://tailscale.com/kb/1085/auth-keys
env
TS_AUTH_KEY=tskey-auth-xxxxxxxxxxx
Enabling on Linux Mint
My tailnet uses Magic DNS which sets the nameserver for all devices on
a tailnet to 100.100.100.100, but since this is a DNS server specific
to a subset of systems we want to use the IP of the dnscrypt-proxy
device instead.
[Magic DNS]:
https://tailscale.com/kb/1081/magicdns
After bringing up the stack with docker compose up, the tailscale
container will authenticate to the tailnet and have an Tailscale IP
(eg 100.112.129.40). This IP is then added to the laptops
/etc/resolv.conf,
nameserver 100.112.129.40
search tailnet-3831.ts.net
Tailscale will keep trying to revert this, so to keep the settings
permanent, /etc/resolv.conf is set to immutable with
chattr +i /etc/resolv.conf.
To test DNS is working, looking for more “adult” content on youtube
will give a message similar to “your Google workspace administrator
has restricted some content”.
Verify in container logs with dig m.youtube.com @100.76.233.91 (where
the IP is your Tailscale container IP) and check the logs for messages
similar to 127.0.0.1 m.youtube.com A CLOAK 0ms -.