From a2a8f0f06a12bb9a52f03128066ab4d6aea93d2d Mon Sep 17 00:00:00 2001
From: Seji64 <seji@tihoda.de>
Date: Fri, 20 Sep 2024 15:31:48 +0200
Subject: [PATCH] Add DoT support

---
 .github/workflows/docker-publish-dev.yml | 18 ++++-----
 Dockerfile                               | 13 ++++++-
 README.md                                |  4 ++
 configs/dnsdist/dnsdist.conf.template    | 20 ++++++++--
 docker-compose.acme.sh-dot.yml           | 47 ++++++++++++++++++++++++
 docker-compose.dot.yml                   | 21 +++++++++++
 docker-compose.yml                       |  3 +-
 entrypoint.sh                            | 17 +++++++++
 8 files changed, 126 insertions(+), 17 deletions(-)
 create mode 100644 docker-compose.acme.sh-dot.yml
 create mode 100644 docker-compose.dot.yml

diff --git a/.github/workflows/docker-publish-dev.yml b/.github/workflows/docker-publish-dev.yml
index 4f13c84..15a2a8c 100644
--- a/.github/workflows/docker-publish-dev.yml
+++ b/.github/workflows/docker-publish-dev.yml
@@ -8,15 +8,11 @@ name: Docker MultiArch Build Dev
 on:
   workflow_dispatch:
   push:
-    branches: [ dev ]
+    branches: [ 76-add-doh-and-dot-support ]
     paths-ignore:
       - '**/README.md'
     # Publish semver tags as releases.
     tags: [ 'v*.*.*' ]
-  pull_request:
-    branches: [ main ]
-    paths-ignore:
-      - '**/README.md'
 
 env:
   # Use docker.io for Docker Hub if empty
@@ -38,19 +34,19 @@ jobs:
 
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
 
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v2
+        uses: docker/setup-qemu-action@v3
 
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v2
+        uses: docker/setup-buildx-action@v3
 
       # Login against a Docker registry except on PR
       # https://github.com/docker/login-action
       - name: Log into registry ${{ env.REGISTRY }}
         if: github.event_name != 'pull_request'
-        uses: docker/login-action@v2
+        uses: docker/login-action@v3
         with:
           registry: ${{ env.REGISTRY }}
           username: ${{ github.actor }}
@@ -60,7 +56,7 @@ jobs:
       # https://github.com/docker/metadata-action
       - name: Extract Docker metadata
         id: meta
-        uses: docker/metadata-action@v4
+        uses: docker/metadata-action@v5
         with:
           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
           tags: |
@@ -77,7 +73,7 @@ jobs:
       # https://github.com/docker/build-push-action
       - name: Build and push Docker image
         id: build-and-push
-        uses: docker/build-push-action@v3
+        uses: docker/build-push-action@v5
         with:
           context: .
           platforms: linux/amd64,linux/arm/v7,linux/arm64
diff --git a/Dockerfile b/Dockerfile
index 8ed1fdb..8e4d303 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,16 +6,23 @@ ENV DNSDIST_BIND_IP=0.0.0.0
 ENV ALLOWED_CLIENTS=127.0.0.1
 ENV ALLOWED_CLIENTS_FILE=
 ENV EXTERNAL_IP=
+
+ENV DNSDIST_ENABLE_DOT=false
+ENV DNSDIST_DOT_CERT_TYPE=auto-self
+
 ENV DNSDIST_WEBSERVER_PASSWORD=
 ENV DNSDIST_WEBSERVER_API_KEY=
 ENV DNSDIST_WEBSERVER_NETWORKS_ACL="127.0.0.1, ::1"
+
 ENV DNSDIST_UPSTREAM_CHECK_INTERVAL=10
 ENV DNSDIST_UPSTREAM_POOL_NAME="upstream"
+
 ENV DNSDIST_RATE_LIMIT_DISABLE=false
 ENV DNSDIST_RATE_LIMIT_WARN=800
 ENV DNSDIST_RATE_LIMIT_BLOCK=1000
 ENV DNSDIST_RATE_LIMIT_BLOCK_DURATION=360
 ENV DNSDIST_RATE_LIMIT_EVAL_WINDOW=60
+
 ENV SPOOF_ALL_DOMAINS=false
 ENV DYNDNS_CRON_SCHEDULE="*/15 * * * *"
 ENV INSTALL_DEFAULT_DOMAINS=true
@@ -28,6 +35,7 @@ EXPOSE 5300/udp
 EXPOSE 8080/tcp
 EXPOSE 8443/tcp
 EXPOSE 8083/tcp
+EXPOSE 8530/tcp
 
 RUN echo "I'm building for $TARGETPLATFORM"
 
@@ -38,10 +46,13 @@ RUN apk update && apk upgrade
 RUN addgroup snidust && adduser -D -H -G snidust snidust
 
 # Install needed packages and clean up
-RUN apk add --no-cache jq tini dnsdist curl bash gnupg procps ca-certificates openssl dog lua5.4-filesystem ipcalc libcap nginx nginx-mod-stream supercronic && rm -rf /var/cache/apk/*
+RUN apk add --no-cache jq tini dnsdist curl bash gnupg procps ca-certificates openssl dog lua5.4-filesystem ipcalc libcap nginx nginx-mod-stream supercronic && \
+    rm -f /etc/nginx/conf.d/*.conf && \
+    rm -rf /var/cache/apk/*
 
 # Setup Folder(s)
 RUN mkdir -p /etc/dnsdist/conf.d && \
+    mkdir -p /etc/dnsdist/certs && \
     mkdir -p /etc/snidust/domains.d && \
     mkdir -p /etc/sniproxy/ && \
     mkdir -p /var/lib/snidust/domains.d
diff --git a/README.md b/README.md
index b3d604f..21b032b 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,10 @@ In case Systemd is already using port 53 you can follow this [Guide](https://www
 
 ## Advanced setups
 
+### DoT
+
+For examples how to use an setup DoT see `docker-compose.dot.yml` and `docker-compose.acme.sh-dot.yml`
+
 ### Disable installtion of repo default domains
 
 If do not want use the default domain lists of this repo, you can disable this by setting the environment variable `INSTALL_DEFAULT_DOMAINS` to `false`.
diff --git a/configs/dnsdist/dnsdist.conf.template b/configs/dnsdist/dnsdist.conf.template
index c4e29fb..bdee0b1 100644
--- a/configs/dnsdist/dnsdist.conf.template
+++ b/configs/dnsdist/dnsdist.conf.template
@@ -20,10 +20,20 @@ fi
 echo "end"
 echo ""
 
+if [ "${DNSDIST_ENABLE_DOT}" == "true" ] && [ "${DNSDIST_DOT_CERT_TYPE}" == "auto-self" ]; then
+    echo "newTLSCertificate('/etc/dnsdist/certs/tls.pem', {key='/etc/dnsdist/certs/tls.key'})"
+fi
+
 echo ""
-echo "-- Add Bind"
+echo "-- Add plain DNS bind"
 echo "addLocal('${DNSDIST_BIND_IP}:5300')"
 echo ""
+if [ "${DNSDIST_ENABLE_DOT}" == "true" ]; then
+    echo "-- Add DoT bind"
+    echo "addTLSLocal('${DNSDIST_BIND_IP}:8530','/etc/dnsdist/certs/tls.pem','/etc/dnsdist/certs/tls.key')"
+else
+    echo "-- TLS Endpoints disabled"
+fi
 
 echo "-- Include Config"
 echo "includeDirectory(\"/etc/dnsdist/conf.d\")"
@@ -59,14 +69,18 @@ if [ "${DNSDIST_DEBUG}" == "true" ]; then
     echo ""
 fi
 
-echo "-- query reload.blocklist.unblockdock.local to reload Blocklist"
+echo "-- query reload.domainlist.snidust.local to reload Blocklist"
 echo "addAction(AndRule({QNameRule(\"reload.domainlist.snidust.local\"),QTypeRule(\"A\")}),LuaAction(ReloadBlocklist))"
 echo ""
 
-echo "-- query reload.acl.unblockdock.local to reload Blocklist"
+echo "-- queryreload.acl.snidust.local to reload Blocklist"
 echo "addAction(AndRule({QNameRule(\"reload.acl.snidust.local\"),QTypeRule(\"A\")}),LuaAction(ReloadACL))"
 echo ""
 
+echo "-- queryreload.certs.snidust.local to reload certificates used for DoT"
+echo "addAction(AndRule({QNameRule(\"reload.certs.snidust.local\"),QTypeRule(\"A\")}),reloadAllCertificates())"
+echo ""
+
 if [ "${SPOOF_ALL_DOMAINS}" == "true" ]; then
     echo " -- rewrite it for ALL Domains"
     echo "addAction(AllRule(), SpoofAction(\"${EXTERNAL_IP}\"))"
diff --git a/docker-compose.acme.sh-dot.yml b/docker-compose.acme.sh-dot.yml
new file mode 100644
index 0000000..e3dd5bb
--- /dev/null
+++ b/docker-compose.acme.sh-dot.yml
@@ -0,0 +1,47 @@
+# NOTE: This is just an example (which is also not fully tested yet) how you could use sniDust with DoT and a trusted certificate.
+# Is uses acme.sh and the DNS-01 method in conjuntion with cloudflare. For other options using acme.sh please see here: https://github.com/acmesh-official/acme.sh/wiki/
+#
+#
+# You need run acme.sh ONCE manually. After the container will manually renew certs and call snidust to reload certs
+#  docker  exec  \
+#    -e CF_Token="TOKEN" \
+#    -e CF_Email="abc@example.com" \
+#    acme.sh --issue -d dot.example.com  --dns dns_cf
+#
+volumes:
+    acme_sh:
+      driver: local
+
+services:
+    acme.sh:
+        container_name: acme.sh
+        image: neilpang/acme.sh
+        command: daemon
+        volumes:
+          - acme_sh:/acme.sh
+          - /var/run/docker.sock:/var/run/docker.sock
+        environment:
+          - DEPLOY_DOCKER_CONTAINER_LABEL=sh.acme.autoload.domain=dot.example.com
+          - DEPLOY_DOCKER_CONTAINER_KEY_FILE=/etc/dnsdist/cert/tls.key
+          - DEPLOY_DOCKER_CONTAINER_CERT_FILE="/etc/dnsdist/cert/cert.pem"
+          - DEPLOY_DOCKER_CONTAINER_CA_FILE="/etc/dnsdist/cert/ca.pem"
+          - DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/etc/dnsdist/cert/tls.pem"
+          - DEPLOY_DOCKER_CONTAINER_RELOAD_CMD="/usr/bin/dog @127.0.0.1:5300 --short reload.certs.snidust.local"
+    sniDust:
+        container_name: sniDust
+        labels:
+            - sh.acme.autoload.domain=dot.example.com
+        environment:
+            - 'ALLOWED_CLIENTS=127.0.0.1/32, myDynDNSDomain.no-ip.com' # CHANGE THIS
+            - 'EXTERNAL_IP=10.111.123.8' # CHANGE THIS TO YOUR VPS PUBLIC IP
+            - TZ=Europe/Berlin
+            - DNSDIST_ENABLE_DOT=true
+            - DNSDIST_DOT_CERT_TYPE=manual
+        ports:
+            - '443:8443'
+            - '80:8080'
+            - '53:5300/udp'
+            - '53:5300/tcp'
+            - '853:8530/tcp'
+        image: 'ghcr.io/seji64/snidust:main'
+        restart: unless-stopped
diff --git a/docker-compose.dot.yml b/docker-compose.dot.yml
new file mode 100644
index 0000000..6f1274d
--- /dev/null
+++ b/docker-compose.dot.yml
@@ -0,0 +1,21 @@
+services:
+    sniDust:
+        container_name: sniDust
+        environment:
+            - 'ALLOWED_CLIENTS=127.0.0.1/32, myDynDNSDomain.no-ip.com' # CHANGE THIS
+            - 'EXTERNAL_IP=10.111.123.8' # CHANGE THIS TO YOUR VPS PUBLIC IP
+            - TZ=Europe/Berlin
+            - DNSDIST_ENABLE_DOT=true
+            - DNSDIST_DOT_CERT_TYPE=auto-self # Generate selfsinged cert. Can also be set to 'manual'
+        # Uncomment this if you choose manual. Align paths to match your environment -> Just an example!
+        # volumes:
+        #   - /etc/letsencrypt/live/dot.example.com/fullchain.pem:/etc/dnsdist/cert/tls.pem:ro
+        #   - /etc/letsencrypt/live/dot.example.com/privkey.pem:/etc/dnsdist/cert/tls.key:ro
+        ports:
+            - '443:8443'
+            - '80:8080'
+            - '53:5300/udp'
+            - '53:5300/tcp'
+            - '853:8530/tcp'
+        image: 'ghcr.io/seji64/snidust:main'
+        restart: unless-stopped
diff --git a/docker-compose.yml b/docker-compose.yml
index 3da1972..dba77dc 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,5 @@
-version: '3.8'
 services:
-    seji64:
+    sniDust:
         container_name: sniDust
         environment:
             - 'ALLOWED_CLIENTS=127.0.0.1/32, myDynDNSDomain.no-ip.com' # CHANGE THIS
diff --git a/entrypoint.sh b/entrypoint.sh
index f8077ba..0342947 100644
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -1,4 +1,21 @@
 #!/bin/bash -e
+
+# Validate DoT config
+if [ "${DNSDIST_ENABLE_DOT}" == "true" ]; then
+  VALID_CERT_TYPE_VALUES=("auto-self" "manual")
+  if [[ -z "$DNSDIST_DOT_CERT_TYPE" ]]; then
+    echo "The environment variable DNSDIST_DOT_CERT_TYPE is not set."
+    exit 1
+  fi
+
+  if [[ " ${valid_values[*]} " =~ " ${DNSDIST_DOT_CERT_TYPE} " ]]; then
+    echo "The value of DNSDIST_DOT_CERT_TYPE is valid: $DNSDIST_DOT_CERT_TYPE"
+  else
+    echo "Invalid value for DNSDIST_DOT_CERT_TYPE: $DNSDIST_DOT_CERT_TYPE"
+    exit 1
+  fi
+fi
+
 if [ -z "${EXTERNAL_IP}" ];
 then
   echo "[INFO] External IP not set - trying to get IP by myself"