feat: Add dnsdist as backend
All checks were successful
Build Docker / Build Docker (push) Successful in 24s

This commit is contained in:
Nathan Woodburn 2023-12-22 17:41:51 +11:00
parent 59c5fa9b75
commit 279fd4c6df
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
7 changed files with 281 additions and 5 deletions

View File

@ -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. Nathan will then add your IP to the domain which will let you create a certificate for the domain.
## Run with docker ## Install script
```bash ```sh
docker run -d --name hns_doh git.woodburn.au/nathanwoodburn/hns_doh:latest 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 ## Nodes
Load balancing to the following DNS-over-HTTPS providers: 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 | | 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 DNS | https://doh.hnsdns.com/dns-query | Yes | Yes | No | Yes | Yes |
| HNS NS | https://hnsns.net/dns-query | Yes | Yes | No | No | 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 ## Maybe future nodes
| Provider | Reason to not be added | URL | DoH JSON | DoH Wire | DoT | DNS | HIP05 | | 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 | | 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 | | HDNS | Only supports NB domains | https://hdns.io | No | Yes | No | Yes | No |

126
cert.py Normal file
View File

@ -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)

7
cert.sh Normal file
View File

@ -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

27
dnsdist.conf Normal file
View File

@ -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')

48
dnsdist.service Normal file
View File

@ -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

31
install.sh Executable file
View File

@ -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

34
resolved.conf Normal file
View File

@ -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