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.
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.
My original DNS config worked well, but I wanted to add some guardrails specifically for the kids laptops,
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 instead.
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.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:
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
.
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-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
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.
.env
TS_AUTH_KEY=tskey-auth-xxxxxxxxxxx
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.
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 -
.