angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Aiohttp mit benutzerdefinierten DNS-Servern, Unbound und Docker

Entlasten Sie Ihre Python aoihttp -Anwendung, indem Sie Ihrem lokalen System DNS-Auflöser zum Caching hinzufügen.

13 Juli 2023
In Async
post main image
https://www.pexels.com/nl-nl/@cihankahraman

Die Verwendung von aiohttp sieht so einfach aus, ist es aber nicht. Es ist verwirrend. Die 'Client Quickstart'-Dokumentation beginnt mit dem Folgenden:

Hinweis

Legen Sie nicht für jede Anfrage eine Sitzung an. Höchstwahrscheinlich benötigen Sie eine Sitzung pro Anwendung, die alle Anfragen zusammen ausführt.

Komplexere Fälle können eine Session pro Site erfordern, z.B. eine für Github und eine andere für Facebook APIs. Es ist keine gute Idee, für jede Anfrage eine Sitzung zu erstellen.

Eine Sitzung enthält einen Verbindungspool. Die Wiederverwendung von Verbindungen und Keepalives (beide sind standardmäßig aktiviert) können die Gesamtleistung beschleunigen.

Hmmm ... ok ... bitte wiederholen ...

Wie auch immer, das Problem: Ich muss viele verschiedene Websites überprüfen und möchte auch benutzerdefinierte DNS-Server verwenden. Das bedeutet eine Sitzung pro Website. Ich weiß nicht, welche Sites, ich bekomme nur eine Liste von Urls. Wir entscheiden uns also für eine Sitzung pro Url. Und alles läuft über Docker.

In diesem Beitrag füttern wir den aiohttp AsyncResolver mit den IP addresses von zwei Unbound caching DNS Resolvern, Cloudflare oder Quad9, die auf unserem lokalen System laufen. Mein Entwicklungssystem ist wie immer Ubuntu 22.04.

Die Python Anwendung

Unten sehen Sie unsere (unvollständige) Python -Anwendung. Sie verwendet aiohttp , um Websites (Urls) zu überprüfen. Das Skript läuft in einem Python Docker Container. Ich werde Sie hier nicht mit der Einrichtung eines Docker Python Images und Containers langweilen.

Beachten Sie, dass wir einen AsyncResolver für jeden TCPConnector für jede Sitzung mit den IP addresses von Cloudflare oder Quad9 erstellen.

# 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())

Problem: Direkte Verbindung zu entfernten DNS-Servern

Obwohl die obige Methode funktioniert, gibt es einige Probleme. Wenn wir viele Urls überprüfen, müssen wir viele (separate) Anfragen an die DNS-Server von Cloudflare oder Quad9 stellen.

Wir können den TCPConnector wiederverwenden, z.B. indem wir einen Pool von TCPConnectors erstellen, und das DNS-Caching der Konnektoren nutzen. Dies ist eine große Verbesserung, aber noch lange nicht perfekt, da unsere Konnektoren weiterhin "direkt" mit der Außenwelt verbunden sind (über den Resolver).

Die Lösung: Lokale Caching-DNS-Server

Wir können es besser machen, indem wir einen oder mehrere Caching-DNS-Server auf unserem lokalen System betreiben und die AsyncResolvers mit den IP addresses unserer Caching-DNS-Server füttern.

Caching-DNS-Server: Ungebunden

Es gibt viele Docker DNS Server images und ich habe 'Unbound DNS Server Docker Image' ausgewählt, siehe Links unten. Warum? Er ist einfach zu verwenden und leitet standardmäßig Anfragen an einen entfernten DNS-Server, Cloudflare, weiter. Eine nette Funktion ist, dass wir DNS über TLS (DoT) verwenden können. Das bedeutet, dass wir die Anfragen vor (ISP-)Verfolgung schützen.

Da wir mehr als einen lokalen DNS-Server benötigen, kopieren wir zunächst einige Konfigurationsdateien außerhalb des Containers. In dem Verzeichnis, in dem wir den DNS-Server starten, legen wir ein neues Verzeichnis an:

my_conf

Dann starten wir den DNS-Server:

docker run --name=my-unbound mvance/unbound:1.17.0

Und in einem anderen Terminal kopieren wir einige Dateien aus dem Container auf unser System:

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

Stoppen Sie den DNS-Server, indem Sie auf 'CTRL-C' drücken.

Ich habe die folgende Datei docker-compose.yml erstellt:

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:

Wir verbinden uns von der Python -Anwendung Docker -Container mit dem DNS-Server-Container über das Docker -Netzwerk. Dies bedeutet, dass die Ports in der Datei docker-compose.yml nicht angegeben werden müssen. Keine Ports zu veröffentlichen bedeutet mehr Sicherheit. So erstellen Sie das Docker -Netzwerk "unbound_dns_network":

docker network create unbound_dns_network

So starten Sie den DNS-Server:

docker-compose up

Prüfen, ob der DNS-Server funktioniert

Hierfür verwende ich 'netshoot: a Docker + Kubernetes network trouble-shooting swiss-army container', siehe Links unten. Wenn wir ihn starten, verbinden wir uns auch mit dem 'unbound_dns_network':

docker run --rm -it --net=unbound_dns_network nicolaka/netshoot

Dann verwenden wir 'dig', um zu prüfen, ob unser DNS-Server funktioniert.

Beachten Sie, dass wir uns hier auf den Dienstnamen Docker-compose , 'unbound_cloudflare_service', beziehen:

dig @unbound_cloudflare_service -p 53 google.com

Ergebnis:

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

Der 'ANTWORTABSCHNITT' ergibt den IP address. Die Abfragezeit beträgt 488 Millisekunden. Wenn wir den Befehl noch einmal ausführen, erhalten wir das gleiche Ergebnis, aber die Abfragezeit ist (fast) 0. Beachten Sie, dass die IP address unseres lokalen DNS-Serverdienstes ebenfalls angezeigt wird:

172.17.10.3

Auf jeden Fall funktioniert unser lokaler DNS-Server!

Wir können diese Schritte für Quad9 wiederholen. Erstellen Sie ein neues Verzeichnis und kopieren Sie die Dateien aus dem Cloudflare-Setup.

Bearbeiten Sie die Datei docker-compose.yml und ersetzen Sie "cloudflare" durch "quad9".

Bearbeiten Sie die Datei "forward-records.conf":

  • Kommentieren Sie die Zeilen für Cloudflare
  • Dekommentieren Sie die Zeilen für Quad9

Und auf geht's!

Verwendung unserer lokalen DNS-Server in unserem Skript Python

Dies ist der letzte Schritt. Wir müssen zwei Dinge tun:

  • Fügen Sie das 'unbound_dns_network' zu unserem Python Docker Container hinzu.
  • Übersetzen Sie die Namen "unbound_cloudflare_service" und "unbound_quad9_service" in IP addresses.

Das Hinzufügen von 'unbound_dns_network' zu unserem Python Docker Container ist einfach. Wir tun dies auf die gleiche Weise wie in der Datei Unbound docker-compose.yml.

Wir kennen bereits die IP addresses unserer lokalen DSN-Serverdienste, aber sie können sich ändern. Anstatt die IP addresses fest zu kodieren, übersetzen wir die Dienstnamen in IP addresses in unserem Skript Python , indem wir den folgenden Code in unserem Skript, siehe oben, von:

    dns_cloudflare = ['1.1.1.1', '1.0.0.1']
    dns_quad9 = ['9.9.9.9', '149.112.112.112']

zu:

    dns_cloudflare = [socket.gethostbyname('unbound_cloudflare_service')]
    dns_quad9 = [socket.gethostbyname('unbound_quad9_service')]

Dies funktioniert natürlich nur, wenn die lokalen DNS-Serverdienste in Betrieb sind.

Jetzt werden alle DNS-Anfragen von unserer Python -Anwendung an unsere lokalen DNS-Server weitergeleitet!

Zusammenfassung

Wir wollten uns freundlich verhalten und die entfernten DNS-Server nicht mit zu vielen Verbindungen überlasten. Außerdem wollten wir die direkte Verbindung unserer Python -Anwendung zu entfernten DNS-Servern entfernen. Wir erreichten dies, indem wir lokale DNS-Serverdienste einrichteten und unser Python -Skript über das Docker -Netzwerk mit ihnen verbanden.
Wir schufen eine zusätzliche Abhängigkeit, nämlich lokale DNS-Server, entfernten aber auch eine Abhängigkeit. Wenn ein entfernter DNS-Server ausfällt (für einige Zeit), funktioniert unsere Python -Anwendung weiter.

Links / Impressum

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

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.