From 279fd4c6dfd1cbaa94b79eac5f42e3cd8ad5cd52 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Fri, 22 Dec 2023 17:41:51 +1100 Subject: [PATCH] feat: Add dnsdist as backend --- README.md | 13 +++-- cert.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++++ cert.sh | 7 +++ dnsdist.conf | 27 +++++++++++ dnsdist.service | 48 ++++++++++++++++++ install.sh | 31 ++++++++++++ resolved.conf | 34 +++++++++++++ 7 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 cert.py create mode 100644 cert.sh create mode 100644 dnsdist.conf create mode 100644 dnsdist.service create mode 100755 install.sh create mode 100644 resolved.conf diff --git a/README.md b/README.md index 1589d95..5d6b124 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,15 @@ You will need a static IP address that you can host the container on. Nathan will then add your IP to the domain which will let you create a certificate for the domain. -## Run with docker -```bash -docker run -d --name hns_doh git.woodburn.au/nathanwoodburn/hns_doh:latest +## Install script +```sh +git clone https://git.woodburn.au/nathanwoodburn/hns_doh_loadbalancer.git +cd hns_doh_loadbalancer +sudo ./install.sh ``` -Then setup your favourite reverse proxy to the container on port 80 + + ## Nodes Load balancing to the following DNS-over-HTTPS providers: @@ -23,13 +26,13 @@ Load balancing to the following DNS-over-HTTPS providers: | Nathan.Woodburn/ | https://doh.hnshosting.au/dns-query | Yes | Yes | Yes | Yes | Yes | | HNS DNS | https://doh.hnsdns.com/dns-query | Yes | Yes | No | Yes | Yes | | HNS NS | https://hnsns.net/dns-query | Yes | Yes | No | No | Yes | +| Impervious | https://hs.dnssec.dev/dns-query | No | Yes | Yes | No | Yes | ## Maybe future nodes | Provider | Reason to not be added | URL | DoH JSON | DoH Wire | DoT | DNS | HIP05 | | ---------------- | -------------------------- | ---------------------------------------- | -------- | -------- | --- | --- | ----- | | EasyHandshake | Doesn't have HIP5 support | https://easyhandshake.com:8053/dns-query | Yes | Yes | No | No | No | -| Impervious | Doesn't support JSON DoH | https://hs.dnssec.dev/dns-query | No | Yes | Yes | No | Yes | | HDNS | Only supports NB domains | https://hdns.io | No | Yes | No | Yes | No | diff --git a/cert.py b/cert.py new file mode 100644 index 0000000..00362b4 --- /dev/null +++ b/cert.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +import json +import os +import requests +import sys +import time + +### EDIT THESE: Configuration values ### +# CONTACT NATHAN FOR AUTH +AUTH = "your-auth-here" +### DO NOT EDIT BELOW THIS POINT ### + + + + + +# URL to acme-dns instance +ACMEDNS_URL = "https://nathan.woodburn.au/hnsdoh-acme" +# Path for acme-dns credential storage +STORAGE_PATH = "/etc/letsencrypt/acmedns.json" +# Whitelist for address ranges to allow the updates from +# Example: ALLOW_FROM = ["192.168.10.0/24", "::1/128"] +ALLOW_FROM = [] +# Force re-registration. Overwrites the already existing acme-dns accounts. +FORCE_REGISTER = False + +DOMAIN = os.environ["CERTBOT_DOMAIN"] +if DOMAIN.startswith("*."): + DOMAIN = DOMAIN[2:] +VALIDATION_DOMAIN = "_acme-challenge."+DOMAIN +VALIDATION_TOKEN = os.environ["CERTBOT_VALIDATION"] + + +class AcmeDnsClient(object): + """ + Handles the communication with ACME-DNS API + """ + + def __init__(self, acmedns_url): + self.acmedns_url = acmedns_url + + def update_txt_record(self, txt): + """Updates the TXT challenge record to ACME-DNS subdomain.""" + update = {"txt": txt, "auth": AUTH} + headers = {"Content-Type": "application/json"} + res = requests.post(self.acmedns_url, + headers=headers, + data=json.dumps(update)) + if res.status_code == 200: + # Successful update + return + else: + msg = ("Encountered an error while trying to update TXT record in " + "acme-dns. \n" + "------- Request headers:\n{}\n" + "------- Request body:\n{}\n" + "------- Response HTTP status: {}\n" + "------- Response body: {}") + s_headers = json.dumps(headers, indent=2, sort_keys=True) + s_update = json.dumps(update, indent=2, sort_keys=True) + s_body = json.dumps(res.json(), indent=2, sort_keys=True) + print(msg.format(s_headers, s_update, res.status_code, s_body)) + sys.exit(1) + +class Storage(object): + def __init__(self, storagepath): + self.storagepath = storagepath + self._data = self.load() + + def load(self): + """Reads the storage content from the disk to a dict structure""" + data = dict() + filedata = "" + try: + with open(self.storagepath, 'r') as fh: + filedata = fh.read() + except IOError as e: + if os.path.isfile(self.storagepath): + # Only error out if file exists, but cannot be read + print("ERROR: Storage file exists but cannot be read") + sys.exit(1) + try: + data = json.loads(filedata) + except ValueError: + if len(filedata) > 0: + # Storage file is corrupted + print("ERROR: Storage JSON is corrupted") + sys.exit(1) + return data + + def save(self): + """Saves the storage content to disk""" + serialized = json.dumps(self._data) + try: + with os.fdopen(os.open(self.storagepath, + os.O_WRONLY | os.O_CREAT, 0o600), 'w') as fh: + fh.truncate() + fh.write(serialized) + except IOError as e: + print("ERROR: Could not write storage file.") + sys.exit(1) + + def put(self, key, value): + """Puts the configuration value to storage and sanitize it""" + # If wildcard domain, remove the wildcard part as this will use the + # same validation record name as the base domain + if key.startswith("*."): + key = key[2:] + self._data[key] = value + + def fetch(self, key): + """Gets configuration value from storage""" + try: + return self._data[key] + except KeyError: + return None + +if __name__ == "__main__": + # Init + client = AcmeDnsClient(ACMEDNS_URL) + storage = Storage(STORAGE_PATH) + + # Update the TXT record in acme-dns instance + client.update_txt_record(VALIDATION_TOKEN) + # Wait for the DNS to propagate for 60 seconds + time.sleep(60) \ No newline at end of file diff --git a/cert.sh b/cert.sh new file mode 100644 index 0000000..bd4d369 --- /dev/null +++ b/cert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Tell dnsdist to reload the config +dnsdist -c -e 'reloadAllCertificates()' + +# Save last run time +date +%s > last_cert_reload.txt \ No newline at end of file diff --git a/dnsdist.conf b/dnsdist.conf new file mode 100644 index 0000000..633145a --- /dev/null +++ b/dnsdist.conf @@ -0,0 +1,27 @@ +newServer({address="194.50.5.26", name="Nathan.Woodburn/ 1"}) +newServer({address="194.50.5.27", name="Nathan.Woodburn/ 2"}) +newServer({address="194.50.5.28", name="Nathan.Woodburn/ 3"}) +newServer({address="139.144.68.241", name="HNSDNS 1"}) +newServer({address="139.144.68.242", name="HNSDNS 2"}) +newServer({address="192.198.87.44:443", tls="openssl", subjectName="hnsns.net", dohPath="/dns-query", validateCertificates=true, name="HNSNS"}) +newServer({address="178.128.128.181:443", tls="openssl", subjectName="hs.dnssec.dev", dohPath="/dns-query", validateCertificates=true, name="Impervious"}) + + +-- Uncomment to add IPv6 servers +-- newServer({address="2a01:7e01:e002:c300::", name="HNSDNS 3"}) +-- newServer({address="2a01:7e01:e002:c300::", name="HNSDNS 4"}) + +addDOHLocal('0.0.0.0', '/etc/letsencrypt/live/hnsdoh.com/fullchain.pem', '/etc/letsencrypt/live/hnsdoh.com/privkey.pem') +addTLSLocal('0.0.0.0', '/etc/letsencrypt/live/hnsdoh.com/fullchain.pem', '/etc/letsencrypt/live/hnsdoh.com/privkey.pem') +setLocal('0.0.0.0:53') + +addACL('0.0.0.0/0') + +--TODO fix this to redirect to welcome page +-- map = { newDOHResponseMapEntry("^/$", 307, "https://welcome.hnsdoh.com") } +-- dohFE = getDOHFrontend(0) +-- dohFE:setResponsesMap(map) + +-- Feel free to change the control socket key +setKey("csl2icaGACsP3+M9tx55c8+dBxVCnlnqAHEC92P55eo=") +controlSocket('127.0.0.1:5199') diff --git a/dnsdist.service b/dnsdist.service new file mode 100644 index 0000000..63c97d4 --- /dev/null +++ b/dnsdist.service @@ -0,0 +1,48 @@ +[Unit] +Description=DNS Loadbalancer +Documentation=man:dnsdist(1) +Documentation=https://dnsdist.org +Wants=network-online.target +After=network-online.target + +[Service] +ExecStartPre=/usr/bin/dnsdist --check-config +# Note: when editing the ExecStart command, keep --supervised and --disable-syslog +ExecStart=/usr/bin/dnsdist --supervised --disable-syslog +User=root +Group=root +Type=notify +Restart=on-failure +RestartSec=2 +TimeoutStopSec=5 +StartLimitInterval=0 + +# Tuning +LimitNOFILE=16384 +TasksMax=8192 + +# Sandboxing +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE +LockPersonality=true +NoNewPrivileges=true +PrivateDevices=true +PrivateTmp=true +# Setting PrivateUsers=true prevents us from opening our sockets +ProtectClock=true +ProtectControlGroups=true +ProtectHome=true +ProtectHostname=true +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectSystem=full +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +RestrictNamespaces=true +RestrictRealtime=true +RestrictSUIDSGID=true +SystemCallArchitectures=native +SystemCallFilter=~ @clock @debug @module @mount @raw-io @reboot @swap @cpu-emulation @obsolete + +[Install] +WantedBy=multi-user.target diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..6d09aba --- /dev/null +++ b/install.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Verify that script is being run as root +if [ "$EUID" -ne 0 ] + then echo "Please run as root to allow installation of dependencies." + exit +fi + +chmod +x cert.sh +sudo apt-get install -y dnsdist +# Install apt-add-repository +sudo apt-get install -y software-properties-common +# Install certbot +sudo apt-add-repository ppa:certbot/certbot -y +sudo apt install certbot -y +sudo certbot certonly --manual --manual-auth-hook ./cert.py --preferred-challenges dns -d hnsdoh.com --deploy-hook ./cert.sh + +sudo cp ./resolved.conf /etc/systemd/resolved.conf +sudo systemctl restart systemd-resolved + +# Move the conf file to the correct location +sudo cp ./dnsdist.conf /etc/dnsdist/dnsdist.conf +# Replace the user and group in the dnsdist.service file to root +# Like this +# User=root +# Group=root +sudo cp ./dnsdist.service /lib/systemd/system/dnsdist.service +sudo systemctl daemon-reload + +# Restart dnsdist +sudo systemctl restart dnsdist \ No newline at end of file diff --git a/resolved.conf b/resolved.conf new file mode 100644 index 0000000..7557d5f --- /dev/null +++ b/resolved.conf @@ -0,0 +1,34 @@ +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Entries in this file show the compile time defaults. Local configuration +# should be created by either modifying this file, or by creating "drop-ins" in +# the resolved.conf.d/ subdirectory. The latter is generally recommended. +# Defaults can be restored by simply deleting this file and all drop-ins. +# +# Use 'systemd-analyze cat-config systemd/resolved.conf' to display the full config. +# +# See resolved.conf(5) for details. + +[Resolve] +# Some examples of DNS servers which may be used for DNS= and FallbackDNS=: +# Cloudflare: 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 +# Google: 8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.google +# Quad9: 9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net +DNS=1.1.1.1 +#FallbackDNS= +#Domains= +#DNSSEC=no +#DNSOverTLS=no +#MulticastDNS=no +#LLMNR=no +#Cache=no-negative +#CacheFromLocalhost=no +DNSStubListener=no +#DNSStubListenerExtra= +#ReadEtcHosts=yes +#ResolveUnicastSingleLabel=no