DNS over traefik

Intro

dnsmasq, being simple and small, can be a pain to configure. Plus, it doesnt have a GUI (unless you count openwrt).

Anyhow, its time for something better, more easily managed.

Requirements are:

  • adblocking
  • authoritative dns for my custom domain and lan
  • GUI
  • IPv6

The options are:

In first attempt i opted for technitium via docker.

Technitium DNS

Technitium satisfies everything i wanted, but there is one annoyance: GUI looks a bit dated. Since i wont be looking at it often, it doesnt really matter.

The server i have is a fedora server (currently 40) running podman (currently 5.2.3).

Since the service is not available outside the lan, podman is running in rootful mode so i dont have to mess around with ports.

/etc/containers/containers.conf

File /etc/containers/containers.conf controls a bunch of stuff regarding podman. Good docu on what can be set there is at https://man.archlinux.org/man/containers.conf.5.en.

Most of the problems i had when trying this out was from these two config options which must be there.

First, container wouldnt start due to some annoying error with iptables. Seems it was failing due to some kernel regression, so i had to force it to use nftables (which anyway should have been a default since at least 2020).

Second, when running in rootful mode, netavark and its dns friend aardavark wouldnt let traefik open port 53 to forward it to the technitium dns. It seems that i wasnt only person wanting to have dns in a container and proxying requests via traefik, but solution wasnt really published anywhere. People usually just dropped it and went away.

Solution was to move the default port from 53 to something else. When done so, port 53 would be available to traefik.

[network]
firewall_driver = "nftables"
dns_bind_port = 5553

Third thing was systemd-resolved, which was starting on port 53. Disabling it wouldnt really be good, so there is a config option needed to be changed in /etc/systemd/resolved.conf.

Editing /etc/systemd/resolved.conf made it work:

DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com
DNSStubListener=no

/etc/containers/systemd/technitium.yaml

apiVersion: v1
kind: Pod
metadata:
  annotations:
    bind-mount-options: /var/run/podman/podman.sock:z
  creationTimestamp: "2024-02-18T16:02:00Z"
  labels:
    app: technitium
    traefik.enable: true

    traefik.http.routers.technitium.entrypoints: websecure
    traefik.http.routers.technitium.rule: "Host(`technitium.flippityflop.com`)"
    traefik.http.routers.technitium.service: technitium
    traefik.http.routers.technitium.tls: true
    traefik.http.routers.technitium.tls.certresolver: lets-encr-porkbun
    traefik.http.services.technitium.loadbalancer.server.port: 5380

    traefik.http.routers.dns-over-https.entrypoints: websecure
    traefik.http.routers.dns-over-https.rule: "Host(`dns-over-https.flippityflop.com`)"
    traefik.http.routers.dns-over-https.service: dns-over-https
    traefik.http.routers.dns-over-https.tls: true
    traefik.http.routers.dns-over-https.tls.certresolver: lets-encr-porkbun
    traefik.http.services.dns-over-https.loadbalancer.server.port: 8053

    traefik.tcp.routers.dns-over-tls.entrypoints: dns-over-tls
    traefik.tcp.routers.dns-over-tls.service: dns-over-tls
    traefik.tcp.services.dns-over-tls.loadbalancer.server.port: 853
    traefik.tcp.routers.dns-over-tls.rule: "HostSNI(`*`)"

    traefik.tcp.routers.dns-tcp.entrypoints: dns-tcp
    traefik.tcp.routers.dns-tcp.service: dns-tcp
    traefik.tcp.services.dns-tcp.loadbalancer.server.port: 53
    traefik.tcp.routers.dns-tcp.rule: "HostSNI(`*`)"

    traefik.udp.routers.dns-udp.entrypoints: dns-udp
    traefik.udp.routers.dns-udp.service: dns-udp
    traefik.udp.services.dns-udp.loadbalancer.server.port: 53
  name: technitium-pod
spec:
  securityContext:
    seLinuxOptions:
      type: spc_t
  containers:
    - image: docker.io/technitium/dns-server:latest
      name: technitium
      args:
      env:
        - name: DNS_SERVER_DOMAIN
          value: flippityflop.com
        - name: DNS_SERVER_ADMIN_PASSWORD
          value: x
        - name: DNS_SERVER_FORWARDERS
          value: 1.1.1.1
      volumeMounts:
        - mountPath: /etc/dns/:z
          name: technitium-etc
          readOnly: false
  restartPolicy: Always
  volumes:
    - hostPath:
        path: /data/technitium/
        type: Directory
      name: technitium-etc

/etc/containers/systemd/technitium.kube

[Unit]
Description=Technitium DNS Server
After=network.target

[Kube]
Yaml=technitium.yaml
Network=systemd-traefik

[Install]
WantedBy=multi-user.target default.target

Traefik

Second thing that took a while to figure out is that i forgot to export ports on traefik. Port 53 should be exported via ports in the kube spec. Hving an entry point is not enough, so update your yaml configuration.

This is a reminder to NOT FORGET IT AGAIN.

/etc/containers/systemd/traefik.yaml

apiVersion: v1
kind: Pod
metadata:
  annotations:
    bind-mount-options: /var/run/podman/podman.sock:z
  creationTimestamp: "2024-02-18T16:02:00Z"
  labels:
    app: traefik
    traefik.enable: true
    traefik.http.routers.traefik.entrypoints: websecure
    traefik.http.routers.traefik.rule: "Host(`traefik.flippityflop.com`)"
    traefik.http.routers.traefik.service: api@internal
    traefik.http.routers.traefik.tls: true
    traefik.http.routers.traefik.tls.certresolver: lets-encr-porkbun
  name: traefik-pod
spec:
  securityContext:
    seLinuxOptions:
      type: spc_t
  containers:
  - image: docker.io/traefik:latest
    name: traefik
    args:
    ports:
    - containerPort: 443
      hostPort: 443
      protocol: TCP
    - containerPort: 80
      hostPort: 80
      protocol: TCP
    - containerPort: 53
      hostPort: 53
      protocol: UDP
    - containerPort: 53
      hostPort: 53
      protocol: TCP
    env:
    - name: PORKBUN_API_KEY
      value: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    - name: PORKBUN_SECRET_API_KEY
      value: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
    volumeMounts:
    - mountPath: /etc/traefik/traefik.yaml
      name: traefik-static
      readOnly: true
    - mountPath: /etc/traefik/acme.json:z
      name: acme
      readOnly: false
    - mountPath: /etc/traefik/porkbun.env
      name: porkbun-env
      readOnly: true
    - mountPath: /var/run/docker.sock:z
      name: podman-socket
      readOnly: false
  restartPolicy: Always
  volumes:
  - hostPath:
      path: /data/traefik/config/traefik-static.yaml
      type: File
    name: traefik-static
  - hostPath:
      path: /data/traefik/config/traefik-dynamic.yaml
      type: File
    name: traefik-dynamic
  - hostPath:
      path: /data/traefik/config/acme.json
      type: File
    name: acme
  - hostPath:
      path: /data/traefik/config/porkbun.env
      type: File
    name: porkbun-env
  - hostPath:
      path: /var/run/podman/podman.sock
      type: File
    name: podman-socket

/etc/containers/systemd/traefik.kube

[Unit]
Description=The sleep container
After=network.target

[Kube]
Yaml=traefik.yaml
Network=systemd-traefik

[Install]
WantedBy=multi-user.target default.target

/data/traefik/config/traefik-static.yaml

## STATIC CONFIGURATION
log:
  level: DEBUG

api:
  insecure: false
  dashboard: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"
  dns-over-tls:
    address: ":853"
  dns-over-quic:
    address: ":853/udp"
  dns-tcp:
    address: ":53"
  dns-udp:
    address: ":53/udp"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    watch: true

certificatesResolvers:
  lets-encr-porkbun:
    acme:
      email: you@magnificent.com
      storage: /etc/traefik/acme.json
      dnsChallenge:
        provider: porkbun
        delayBeforeCheck: 0
        resolvers:
          - "curitiba.ns.porkbun.com"
          - "1.1.1.1:53"

/data/traefik/config/traefik-dynamic.yaml

## DYNAMIC CONFIGURATION
http:
  routers:
    api:
      entryPoints: [ websecure ]
      rule: "Host(`traefik.flippityflop.com`)"
      service: "api@internal"
      tls:
        certResolver: lets-encr-porkbun
        domains:
          - main: "traefik.flippityflop.com"

Podman

Of course, after setting this up, few other commands are needed:

systemctl daemon-reload
systemctl restart traefik
systemctl restart technitium