Aiohttp con servidores DNS personalizados, Unbound y Docker
Descargue su aplicación Python aoihttp añadiendo resolvedores DNS de caché a su sistema local.
Utilizar aiohttp parece muy fácil, pero no lo es. Es confuso. La documentación 'Client Quickstart' comienza con lo siguiente:
Nota No cree una sesión por solicitud. Lo más probable es que necesite una sesión por solicitud que realice todas las solicitudes juntas. Casos más complejos pueden requerir una sesión por sitio, por ejemplo, una para Github y otra para Facebook APIs. De todos modos hacer una sesión para cada petición es una muy mala idea. Una sesión contiene un pool de conexiones en su interior. La reutilización de la conexión y los keep-alives (ambos están activados por defecto) pueden acelerar el rendimiento total. |
Hmmm ... ok ... repite por favor ...
De todos modos, el problema: Tengo que comprobar muchos sitios diferentes, y también quiero usar servidores DNS personalizados. Esto significa una sesión por sitio. No sé qué sitios, acabo de obtener una lista de Urls. Así que vamos para una sesión por Url. Y todo usando Docker.
En este post alimentamos el aiohttp AsyncResolver con los IP addresses de dos resolvers DNS de caché Unbound, Cloudflare o Quad9, corriendo en nuestro sistema local. Como siempre mi sistema de desarrollo es Ubuntu 22.04.
La aplicación Python
A continuación se muestra nuestra aplicación (incompleta) Python . Utiliza aiohttp para comprobar sitios (Urls). El script se ejecuta utilizando un contenedor Python Docker . No voy a aburrirte aquí con la configuración de una imagen y un contenedor Docker Python .
Tenga en cuenta que creamos un AsyncResolver para cada TCPConnector para cada sesión utilizando los IP addresses de Cloudflare o Quad9.
# check_urls.py
import asyncio
import aiodns
import aiohttp
import logging
import os
import socket
import sys
def get_logger(
console_log_level=logging.DEBUG,
file_log_level=logging.DEBUG,
log_file=os.path.splitext(__file__)[0] + '.log',
):
logger_format = '%(asctime)s %(levelname)s [%(filename)-30s%(funcName)30s():%(lineno)03s] %(message)s'
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
if console_log_level:
# console
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(console_log_level)
console_handler.setFormatter(logging.Formatter(logger_format))
logger.addHandler(console_handler)
if file_log_level:
# file
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(file_log_level)
file_handler.setFormatter(logging.Formatter(logger_format))
logger.addHandler(file_handler)
return logger
logger = get_logger(file_log_level=None)
async def check_url(task_number, url, nameservers=None):
logger.debug(f'[{task_number}] url = {url}, nameservers = {nameservers}')
resolver = None
if nameservers:
resolver = aiohttp.resolver.AsyncResolver(nameservers=nameservers)
connector=aiohttp.TCPConnector(
limit=1,
use_dns_cache=True,
ttl_dns_cache=300,
family=socket.AF_INET,
resolver=resolver
)
async with aiohttp.ClientSession(
connector=connector,
# if we want to reuse the connector with other sessions,
# we must not close it: connector_owner=False
connector_owner=True,
) as session:
async with session.get(
url,
) as client_response:
logger.debug(f'[{task_number}] status = {client_response.status}')
logger.debug(f'[{task_number}] url = {client_response.url}')
logger.debug(f'[{task_number}] content_type = {client_response.headers.get("Content-Type", None)}')
logger.debug(f'[{task_number}] charset = {client_response.charset}')
async def main():
logger.debug(f'()')
dns_cloudflare = ['1.1.1.1', '1.0.0.1']
dns_quad9 = ['9.9.9.9', '149.112.112.112']
sites = [
('http://www.example.com', dns_cloudflare),
('http://www.example.org', dns_quad9)
]
tasks = []
for task_number, site in enumerate(sites):
url, nameservers = site
task = asyncio.create_task(check_url(task_number, url, nameservers))
tasks.append(task)
for task in tasks:
await task
logger.debug(f'ready')
asyncio.run(main())
Problema: Conexión directa a servidores DNS remotos
Aunque lo anterior funciona tiene algunos problemas. Si comprobamos muchas Urls entonces estamos disparando muchas peticiones (separadas) a los servidores DNS de Cloudflare o Quad9.
Podemos reutilizar el TCPConnector, por ejemplo, mediante la creación de un grupo de TCPConnectors, y utilizar el almacenamiento en caché de DNS de los conectores. Esto es una gran mejora, pero aún está lejos de ser perfecto porque nuestros conectores siguen conectados "directamente" al mundo exterior (a través del resolver).
Solución: Servidores DNS de caché locales
Podemos hacerlo mejor ejecutando uno o más servidores DNS de caché en nuestro sistema local, y alimentando los AsyncResolvers con los IP addresses de nuestros servidores DNS de caché.
Servidor DNS de caché: Sin consolidar
Hay un montón de Docker servidor DNS images y he seleccionado 'Sin consolidar servidor DNS Docker imagen', ver enlaces más abajo. ¿Por qué? Es fácil de usar, y por defecto reenvía las consultas a un servidor DNS remoto, Cloudflare. Una buena característica es que podemos usar DNS sobre TLS (DoT). Esto significa que protegemos las peticiones del rastreo (ISP).
Como queremos más de un servidor DNS local, primero copiamos algunos archivos de configuración fuera del contenedor. En el directorio donde iniciamos el servidor DNS, creamos un nuevo directorio:
my_conf
Luego iniciamos el servidor DNS:
docker run --name=my-unbound mvance/unbound:1.17.0
Y en otro terminal, copiamos algunos ficheros de dentro del contenedor a nuestro sistema:
mkdir my_conf
docker cp my-unbound:/opt/unbound/etc/unbound/forward-records.conf my_conf
docker cp my-unbound:/opt/unbound/etc/unbound/a-records.conf my_conf
Detenemos el servidor DNS pulsando 'CTRL-C'.
He creado el siguiente archivo docker-compose.yml:
version: '3'
services:
unbound_cloudflare_service:
image: "mvance/unbound:1.17.0"
container_name: unbound_cloudflare_container
networks:
- dns
volumes:
- type: bind
read_only: true
source: ./my_conf/forward-records.conf
target: /opt/unbound/etc/unbound/forward-records.conf
- type: bind
read_only: true
source: ./my_conf/a-records.conf
target: /opt/unbound/etc/unbound/a-records.conf
restart: unless-stopped
networks:
dns:
external: true
name: unbound_dns_network
volumes:
mydata:
Nos estamos conectando desde la aplicación Python contenedor Docker al contenedor del servidor DNS utilizando Docker red. Esto significa que no hay necesidad de especificar los puertos en el archivo docker-compose.yml. No publicar puertos significa mayor seguridad. Para crear la red Docker 'unbound_dns_network':
docker network create unbound_dns_network
Para iniciar el servidor DNS:
docker-compose up
Comprobar si el servidor DNS está funcionando
Para ello utilizo 'netshoot: a Docker + Kubernetes network trouble-shooting swiss-army container', ver enlaces más abajo. Cuando lo iniciamos, también nos conectamos a 'unbound_dns_network':
docker run --rm -it --net=unbound_dns_network nicolaka/netshoot
A continuación, utilizamos 'dig' para comprobar si nuestro servidor DNS está funcionando.
Ten en cuenta que aquí nos referimos al nombre del servicio Docker-compose , 'unbound_cloudflare_service':
dig @unbound_cloudflare_service -p 53 google.com
Resultado:
---
; <<>> DiG 9.18.13 <<>> @unbound_cloudflare_service -p 53 google.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 55895
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;google.com. IN A
;; ANSWER SECTION:
google.com. 300 IN A 142.250.179.174
;; Query time: 488 msec
;; SERVER: 172.17.10.3#53(unbound_cloudflare_service) (UDP)
;; WHEN: Wed Jul 12 16:32:56 UTC 2023
;; MSG SIZE rcvd: 55
La 'SECCIÓN DE RESPUESTAS' da el IP address. El tiempo de consulta es de 488 milisegundos. Si ejecutamos el comando de nuevo, obtendremos el mismo resultado pero el tiempo de consulta será (cercano a) 0. Observa que también se muestra el IP address de nuestro servicio de servidor DNS local:
172.17.10.3
En cualquier caso, ¡nuestro servidor DNS local funciona!
Podemos repetir estos pasos para Quad9. Crear un nuevo directorio y copiar los archivos de la configuración de Cloudflare.
Edita el archivo docker-compose.yml y sustituye 'cloudflare' por 'quad9'.
Edite el archivo 'forward-records.conf':
- Comente las líneas para Cloudflare
- Descomente las líneas para Quad9
Y ¡arriba!
Usando nuestros servidores DNS locales en nuestro script Python
Este es el último paso. Debemos hacer dos cosas:
- Añadir el 'unbound_dns_network' a nuestro contenedor Python Docker .
- Traducir los nombres 'unbound_cloudflare_service', y 'unbound_quad9_service' a IP addresses.
Añadir el 'unbound_dns_network' a nuestro contenedor Python Docker es fácil. Lo hacemos de la misma forma que en el archivo Unbound docker-compose.yml.
Ya conocemos los IP addresses de nuestros servicios de servidor DSN locales, pero pueden cambiar. En lugar de codificar los IP addresses, traducimos los nombres de servicio a IP addresses en nuestro script Python , cambiando el siguiente código en nuestro script, ver arriba, de:
dns_cloudflare = ['1.1.1.1', '1.0.0.1']
dns_quad9 = ['9.9.9.9', '149.112.112.112']
a:
dns_cloudflare = [socket.gethostbyname('unbound_cloudflare_service')]
dns_quad9 = [socket.gethostbyname('unbound_quad9_service')]
Por supuesto, esto sólo funciona si los servicios del servidor DNS local son up-and-running.
Ahora todas las peticiones DNS de nuestra aplicación Python se dirigen a nuestros servidores DNS locales.
Resumen
Queríamos comportarnos de forma amigable y no sobrecargar los servidores DNS remotos con demasiadas conexiones. También queríamos eliminar la conexión directa de nuestra aplicación Python a servidores DNS remotos. Lo hemos conseguido creando servicios de servidores DNS locales y conectando nuestro script Python a ellos, utilizando la red Docker .
Hemos creado una depencia extra, servidores DNS locales, pero también hemos eliminado una depencia. Si un servidor DNS remoto está caído (por algún tiempo), nuestra aplicación Python sigue funcionando.
Enlaces / créditos
Docker Container Published Port Ignoring UFW Rules
https://www.baeldung.com/linux/docker-container-published-port-ignoring-ufw-rules
netshoot: a Docker + Kubernetes network trouble-shooting swiss-army container
https://github.com/nicolaka/netshoot
Unbound
https://nlnetlabs.nl/projects/unbound/about
Unbound DNS Server Docker Image
https://github.com/MatthewVance/unbound-docker
Recientes
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando PyInstaller y Cython para crear un ejecutable de Python
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow