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

Aiohttp с пользовательскими DNS-серверами, Unbound и Docker

Разгрузите приложение Python aoihttp , добавив в локальную систему кэширующие DNS-резольверы.

13 июля 2023
В Async
post main image
https://www.pexels.com/nl-nl/@cihankahraman

Использование aiohttp выглядит так просто, но это не так. Это сбивает с толку. Документация 'Client Quickstart' начинается со следующего:

Примечание

Не создавайте сессию для каждого запроса. Скорее всего, вам нужна сессия для каждого приложения, которое выполняет все запросы вместе.

В более сложных случаях может потребоваться сессия для каждого сайта, например, одна для Github, а другая для Facebook APIs. В любом случае создавать сессию для каждого запроса - очень плохая идея.

Сессия содержит внутри себя пул соединений. Повторное использование соединений и keep-alives (оба включены по умолчанию) могут ускорить общую производительность.

Хммм... хорошо... повторите, пожалуйста...

В общем, проблема: мне нужно проверить много разных сайтов, и я также хочу использовать пользовательские DNS-серверы. Это означает, что на каждый сайт приходится одна сессия. Я не знаю, какие сайты, я просто получаю список урлов. Поэтому мы используем сессию для каждого урла. И все это с использованием Docker.

В этом посте мы кормим aiohttp AsyncResolver с IP addresses двух кэширующих DNS-резольверов Unbound, Cloudflare или Quad9, работающих на нашей локальной системе. Как обычно, моей системой разработки является Ubuntu 22.04.

Приложение Python

Ниже представлено наше (неполное) приложение Python . Оно использует aiohttp для проверки сайтов (Urls). Сценарий выполняется с помощью контейнера Python Docker . Я не буду утомлять вас настройкой образа и контейнера Docker Python .

Обратите внимание, что мы создаем AsyncResolver для каждого сеанса TCPConnector , используя IP addresses Cloudflare или 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())

Проблема: Прямое подключение к удаленным DNS-серверам

Хотя описанное выше работает, оно имеет некоторые проблемы. Если мы проверяем большое количество урлов, то мы отправляем много (отдельных) запросов к DNS-серверам Cloudflare или Quad9.

Мы можем повторно использовать TCPConnector, например, создав пул TCPConnector, и использовать DNS-кэширование коннекторов. Это значительное улучшение, но оно все еще далеко от совершенства, поскольку наши коннекторы остаются "напрямую" подключенными к внешнему миру (через резолвер).

Решение: Локальные кэширующие DNS-серверы

Мы можем добиться большего, запустив один или несколько кэширующих DNS-серверов в нашей локальной системе и снабдив AsyncResolvers IP addresses наших кэширующих DNS-серверов.

Кэширующий DNS-сервер: Несвязанный

Существует множество Docker DNS-серверов images , и я выбрал 'Unbound DNS Server Docker Image', см. ссылки ниже. Почему? Он прост в использовании и по умолчанию перенаправляет запросы на удаленный DNS-сервер Cloudflare. Приятной особенностью является то, что мы можем использовать DNS over TLS (DoT). Это означает, что мы защищаем запросы от отслеживания (провайдером).

Поскольку мы хотим иметь более одного локального DNS-сервера, сначала скопируем некоторые конфигурационные файлы за пределы контейнера. В каталоге, где мы запускаем DNS-сервер, мы создадим новый каталог:

my_conf

, затем запускаем DNS-сервер:

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

И в другом терминале копируем некоторые файлы из контейнера в нашу систему:

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

Останавливаем DNS-сервер, нажав 'CTRL-C'.

Я создал следующий файл 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:

Мы подключаемся из контейнера Python приложения Docker к контейнеру DNS-сервера, используя сеть Docker . Это означает, что нет необходимости указывать порты в файле docker-compose.yml. Отсутствие портов означает повышенную безопасность. Создать сеть Docker 'unbound_dns_network':

docker network create unbound_dns_network

Запустить DNS-сервер:

docker-compose up

Проверить, работает ли DNS-сервер.

Для этого я использую 'netshoot: a Docker + Kubernetes network trouble-shooting swiss-army container', см. ссылки ниже. При его запуске мы также подключаемся к 'unbound_dns_network':

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

Затем мы используем 'dig' для проверки работоспособности нашего DNS-сервера.

Обратите внимание, что здесь мы ссылаемся на имя службы Docker-compose , 'unbound_cloudflare_service':

dig @unbound_cloudflare_service -p 53 google.com

Результат:

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

'РАЗДЕЛ ОТВЕТОВ' выдает IP address. Время выполнения запроса составляет 488 миллисекунд. Если мы выполним команду еще раз, то получим тот же результат, но время запроса будет (близко) к 0. Обратите внимание, что IP address нашего локального DNS-сервера также показан:

172.17.10.3

В общем, наш локальный DNS-сервер работает!

Мы можем повторить эти шаги для Quad9. Создайте новый каталог и скопируйте в него файлы из установки Cloudflare.

Отредактируйте файл docker-compose.yml и замените 'cloudflare' на 'quad9'.

Отредактируйте файл 'forward-records.conf':

  • Закомментируйте строки для Cloudflare
  • Декомментируйте строки для Quad9

И поднимайте!

Использование наших локальных DNS-серверов в скрипте Python

Это последний шаг. Мы должны сделать две вещи:

  • Добавить 'unbound_dns_network' в наш контейнер Python Docker .
  • Переводим имена 'unbound_cloudflare_service' и 'unbound_quad9_service' в IP addresses.

Добавить 'unbound_dns_network' в наш контейнер Python Docker очень просто. Мы делаем это так же, как и в файле Unbound docker-compose.yml.

Мы уже знаем IP address наших локальных служб DSN-сервера, но они могут меняться. Вместо того чтобы жестко кодировать IP addresses, мы переводим имена сервисов в IP addresses в нашем скрипте Python , изменяя следующий код в нашем скрипте, см. выше, от:

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

на:

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

Конечно, это работает только в том случае, если службы локального DNS-сервера работают.

Теперь все DNS-запросы от нашего приложения Python будут направляться на наши локальные DNS-серверы!

Резюме

Мы хотели вести себя дружелюбно и не перегружать удаленные DNS-серверы большим количеством соединений. Мы также хотели устранить прямое подключение нашего приложения Python к удаленным DNS-серверам. Для этого мы раскрутили локальные службы DNS-серверов и подключили к ним наш скрипт Python , используя сеть Docker .
Мы создали дополнительную депоненту - локальные DNS-серверы, а также удалили депоненту. Если удаленный DNS-сервер не работает (в течение некоторого времени), то наше приложение Python продолжает работать.

Ссылки / кредиты

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

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.