DNS Recursivo Anycast HyperLocal
#!/usr/bin/env bash
cat /var/log/unbound/unbound.log |grep "failure" | awk '{ print $8 }'| sort -u > /tmp/listagem_dominios.txt
for lista in `cat /tmp/listagem_dominios.txt`; do
TESTE1="`dig @8.8.8.8 $lista |grep \"status:\" |grep \"NOERROR\"`"
if [ "$TESTE1" != "" ]; then
TESTE2="`dig @127.0.0.1 $lista |grep \"status:\" |grep \"SERVFAIL\"`"
if [ "$TESTE2" != "" ]; then
echo $lista
unbound-control flush $lista
fi
fi
done
>/var/log/unbound/unbound.log
# chmod 700 /root/scripts/checa_cache.sh # echo "*/1 * * * * root /root/scripts/checa_cache.sh" >> /etc/crontab
Habilitando o DoH (DNS over HTTPS) - opcional
Para habilitar o DoH no Unbound é bem simples mas só é possível com a versão 1.17.1 que vem no bullseye-backports e que virá na próxima versão do Debian 12 (Bookworm). O recurso do DoH vem para trazer mais segurança e privacidade para o usuário. É um recurso muito pouco utilizado ainda mas que seu cliente pode vir a pedir algum dia.
Você precisará gerar certificados SSL legítimos e para isso você poderá usar o Let's Encrypt só que de uma forma não tão convencional.
Na sequência vamos instalar o Let's Encrypt para gerarmos nosso certificado SSL:
# apt install letsencrypt
Escolha um hostname para ser usado no nosso DoH e aponte ele no seu DNS Autoritativo para seus IPs 10.10.10.10 e 10.10.9.9. Aqui vamos usar o seguinte como exemplo: doh.ispup.com.br. Para gerarmos nosso certificado iremos usar o tipo DNS-01, ele não necessita que tenhamos um servidor web rodando no servidor e nem tão pouco levanta um serviço na porta 80 para checar o hostname. Ele utiliza o DNS como validador e vai te solicitar que crie um registro CNAME no seu DNS Autoritativo para provar que você tem o controle sobre aquele hostname. Antes disso vamos instalar um programa em Python para podermos automatizar nossa renovação de certificado no futuro. Esse programa se encontra aqui mas vou deixá-lo abaixo já modificado o interpretador.
Crie o arquivo /etc/letsencrypt/acme-dns-auth.py com o conteúdo abaixo:
#!/usr/bin/env python3
import json
import os
import requests
import sys
### EDIT THESE: Configuration values ###
# URL to acme-dns instance
ACMEDNS_URL = "https://auth.acme-dns.io"
# 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
### DO NOT EDIT BELOW THIS POINT ###
### HERE BE DRAGONS ###
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 register_account(self, allowfrom):
"""Registers a new ACME-DNS account"""
if allowfrom:
# Include whitelisted networks to the registration call
reg_data = {"allowfrom": allowfrom}
res = requests.post(self.acmedns_url+"/register",
data=json.dumps(reg_data))
else:
res = requests.post(self.acmedns_url+"/register")
if res.status_code == 201:
# The request was successful
return res.json()
else:
# Encountered an error
msg = ("Encountered an error while trying to register a new acme-dns "
"account. HTTP status {}, Response body: {}")
print(msg.format(res.status_code, res.text))
sys.exit(1)
def update_txt_record(self, account, txt):
"""Updates the TXT challenge record to ACME-DNS subdomain."""
update = {"subdomain": account['subdomain'], "txt": txt}
headers = {"X-Api-User": account['username'],
"X-Api-Key": account['password'],
"Content-Type": "application/json"}
res = requests.post(self.acmedns_url+"/update",
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)
# Check if an account already exists in storage
account = storage.fetch(DOMAIN)
if FORCE_REGISTER or not account:
# Create and save the new account
account = client.register_account(ALLOW_FROM)
storage.put(DOMAIN, account)
storage.save()
# Display the notification for the user to update the main zone
msg = "Please add the following CNAME record to your main DNS zone:\n{}"
cname = "{} CNAME {}.".format(VALIDATION_DOMAIN, account["fulldomain"])
print(msg.format(cname))
# Update the TXT record in acme-dns instance
client.update_txt_record(account, VALIDATION_TOKEN)
# chmod +x /etc/letsencrypt/acme-dns-auth.py
Usaremos a seguinte instrução para criar nosso certificado:
# certbot certonly --manual --manual-auth-hook /etc/letsencrypt/acme-dns-auth.py --preferred-challenges dns --debug-challenges -d doh.ispup.com.br Saving debug log to /var/log/letsencrypt/letsencrypt.log Plugins selected: Authenticator manual, Installer None Cert is due for renewal, auto-renewing... Renewing an existing certificate for doh.ispup.com.br.br Performing the following challenges: dns-01 challenge for doh.ispup.com.br Running manual-auth-hook command: /etc/letsencrypt/acme-dns-auth.py Output from manual-auth-hook command acme-dns-auth.py: Please add the following CNAME record to your main DNS zone: _acme-challenge.doh.ispup.com.br CNAME b555d682-7b50-45d9-a92f-3c3d187dd4e7.auth.acme-dns.io. Waiting for verification... - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Challenges loaded. Press continue to submit to CA. Pass "-v" for more info about challenges. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Press Enter to Continue
Nesse momento você cria o registro CNAME no seu DNS Autoritativo conforme ele solicitou: _acme-challenge.doh.ispup.com.br IN CNAME b555d682-7b50-45d9-a92f-3c3d187dd4e7.auth.acme-dns.io. e somente depois de criado e checado no DNS, você pressiona o Enter para continuar. Você pode checar dessa forma:
# host -t cname _acme-challenge.doh.ispup.com.br _acme-challenge.doh.ispup.com.br is an alias for b555d682-7b50-45d9-a92f-3c3d187dd4e7.auth.acme-dns.io.
Para que nosso certificado seja automaticamente renovado colocaremos no /etc/crontab a seguinte linha abaixo:
00 00 1 * * root /usr/bin/certbot -q renew
Acima temos a instrução para renovação automática do certificado. Repare que você vai precisar também copiar esse certificado para seus outros servidores, escolha um servidor para manter o certificado sempre atualizado e crie um script que faça a mesma cópia remotamente para os outros servidores. O scp e o rsync são seus aliados nisso.
Configurando o Unbound
Em nosso /etc/unbound/unbound.conf.d/local.conf, adicionaremos no bloco "server:" o seguinte:
interface: 10.10.10.10@443 interface: 10.10.9.9@443 tls-service-key: "/etc/letsencrypt/live/doh.ispup.com.br/privkey.pem" tls-service-pem: "/etc/letsencrypt/live/doh.ispup.com.br/fullchain.pem"
Para usar o recurso do DoH você precisará habilitar o recurso no seu navegador e informar a URL. Vou colocar o exemplo do Google Chrome: Digite chrome://settings/security?search=dns no seu Chrome e ative Usar DNS seguro, selecione Personalizado e adicione nossa URL:

Finalizando
Aqui finalizamos nosso projeto para uma Rede de DNS(s) Recursivos Anycast com Hyperlocal. Esse projeto é escalável, seguro, resiliente e você entregará muito mais qualidade de Internet para o seu cliente. Pare de entregar o 8.8.8.8 para os seus clientes, você está contribuindo para uma Internet mais lenta, sem a qualidade que o seu cliente merece. Investi meu tempo, que é muito pouco, para deixar esse documento para a comunidade, para você melhorar o seu ISP, para dar um UP! nele, então vamos começar 2023 com o pé direito. O que acha?
Como prova de conceito, uma imagem abaixo onde temos uma Rede em produção de DNS(s) Recursivos Anycast e apontando exatamente o momento em que houve alguma situação que fez com que as queries de DNS, convergissem de um node para outro, de forma transparente e automática para o cliente. Podemos notar também que ao ser resolvido o problema, o tráfego retornou para o seu node correto:

KINDNS (Stands for Knowledge-Sharing and Instantiating Norms for DNS and Naming Security)
Achou que havia terminado? Agora que você tem a capacidade de montar uma Rede de DNS Recursivo com todas essas features acima, com todas as ferramentas que foram comentadas, o que acha de certificar o que fez?
Assim como o MANRS veio para certificar nosso sistema de roteamento na Internet, agora temos o KINDNS para certificar que nossos sistemas de DNS estão bem feitos e dentro dos padrões de segurança. Existem 7 ações que podem ser certificadas para nossos DNS Recursivos e estão aqui em https://kindns.org/shared-private-resolvers/. Com essa nossa documentação, se bem aplicada, você pode se candidatar ao KINDNS e ter seu ASN listado aqui https://kindns.org/participants/
Obter e manter o MANRS e KINDNS demonstra seu compromisso com as Boas Práticas, contribui para termos uma Internet mais segura e te abre portas para novos negócios que possam exigir essas conformidades.
Essa documentação foi útil? Compartilhe, divulgue e ajude outras pessoas. Meus contatos podem ser vistos aqui.