Skip to content

Commit

Permalink
Switch to using the AsyncResolver with aiohttp (#114529)
Browse files Browse the repository at this point in the history
* Bump aiodns to 3.2.0

changelog: aio-libs/aiodns@v3.1.1...v3.2.0

* Switch to using the AsyncResolver with aiohttp

This avoids creating executor jobs to do DNS resolution

AsyncResolver was not usable before aio-libs/aiohttp#8270
because it did not fallback to fallback to A records and only returned AAAA records
in most cases when IPv6 was available

This is a backport of aio-libs/aiohttp#8270

* Switch to using the AsyncResolver with aiohttp

This avoids creating executor jobs to do DNS resolution

AsyncResolver was not usable before aio-libs/aiohttp#8270
because it did not fallback to fallback to A records and only returned AAAA records
in most cases when IPv6 was available

This is a backport of aio-libs/aiohttp#8270

* Switch to using the AsyncResolver with aiohttp

This avoids creating executor jobs to do DNS resolution

AsyncResolver was not usable before aio-libs/aiohttp#8270
because it did not fallback to fallback to A records and only returned AAAA records
in most cases when IPv6 was available

This is a backport of aio-libs/aiohttp#8270

* Switch to using the AsyncResolver with aiohttp

This avoids creating executor jobs to do DNS resolution

AsyncResolver was not usable before aio-libs/aiohttp#8270
because it did not fallback to fallback to A records and only returned AAAA records
in most cases when IPv6 was available

This is a backport of aio-libs/aiohttp#8270

* fixes

* fix mocking in next_dns

* fix unmocked calls in blink

* more mocking fixes

* more fixes

* more fixes

* Fix missing mocking in nextdns tests

extracted from #114539

* extract from context
  • Loading branch information
bdraco committed Apr 6, 2024
1 parent 9c27e63 commit f497c46
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 0 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ source = homeassistant
omit =
homeassistant/__main__.py
homeassistant/helpers/signal.py
homeassistant/helpers/backports/*
homeassistant/scripts/__init__.py
homeassistant/scripts/check_config.py
homeassistant/scripts/ensure_config.py
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/helpers/aiohttp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from homeassistant.util import ssl as ssl_util
from homeassistant.util.json import json_loads

from .backports.aiohttp_resolver import AsyncResolver
from .frame import warn_use
from .json import json_dumps

Expand Down Expand Up @@ -310,6 +311,7 @@ def _async_get_connector(
ssl=ssl_context,
limit=MAXIMUM_CONNECTIONS,
limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST,
resolver=AsyncResolver(),
)
connectors[connector_key] = connector

Expand Down
1 change: 1 addition & 0 deletions homeassistant/helpers/backports/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Backports for helpers."""
116 changes: 116 additions & 0 deletions homeassistant/helpers/backports/aiohttp_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Backport of aiohttp's AsyncResolver for Home Assistant.
This is a backport of the AsyncResolver class from aiohttp 3.10.
Before aiohttp 3.10, on system with IPv6 support, AsyncResolver would not fallback
to providing A records when AAAA records were not available.
Additionally, unlike the ThreadedResolver, AsyncResolver
did not handle link-local addresses correctly.
"""

from __future__ import annotations

import asyncio
import socket
import sys
from typing import Any, TypedDict

import aiodns
from aiohttp.abc import AbstractResolver

# This is a backport of https://github.com/aio-libs/aiohttp/pull/8270
# This can be removed once aiohttp 3.10 is the minimum supported version.

_NUMERIC_SOCKET_FLAGS = socket.AI_NUMERICHOST | socket.AI_NUMERICSERV
_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0)


class ResolveResult(TypedDict):
"""Resolve result.
This is the result returned from an AbstractResolver's
resolve method.
:param hostname: The hostname that was provided.
:param host: The IP address that was resolved.
:param port: The port that was resolved.
:param family: The address family that was resolved.
:param proto: The protocol that was resolved.
:param flags: The flags that were resolved.
"""

hostname: str
host: str
port: int
family: int
proto: int
flags: int


class AsyncResolver(AbstractResolver):
"""Use the `aiodns` package to make asynchronous DNS lookups."""

def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the resolver."""
if aiodns is None:
raise RuntimeError("Resolver requires aiodns library")

self._loop = asyncio.get_running_loop()
self._resolver = aiodns.DNSResolver(*args, loop=self._loop, **kwargs) # type: ignore[misc]

async def resolve( # type: ignore[override]
self, host: str, port: int = 0, family: int = socket.AF_INET
) -> list[ResolveResult]:
"""Resolve a host name to an IP address."""
try:
resp = await self._resolver.getaddrinfo(
host,
port=port,
type=socket.SOCK_STREAM,
family=family, # type: ignore[arg-type]
flags=socket.AI_ADDRCONFIG,
)
except aiodns.error.DNSError as exc:
msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed"
raise OSError(msg) from exc
hosts: list[ResolveResult] = []
for node in resp.nodes:
address: tuple[bytes, int] | tuple[bytes, int, int, int] = node.addr
family = node.family
if family == socket.AF_INET6:
if len(address) > 3 and address[3] and _SUPPORTS_SCOPE_ID:
# This is essential for link-local IPv6 addresses.
# LL IPv6 is a VERY rare case. Strictly speaking, we should use
# getnameinfo() unconditionally, but performance makes sense.
result = await self._resolver.getnameinfo(
(address[0].decode("ascii"), *address[1:]),
_NUMERIC_SOCKET_FLAGS,
)
resolved_host = result.node
else:
resolved_host = address[0].decode("ascii")
port = address[1]
else: # IPv4
assert family == socket.AF_INET
resolved_host = address[0].decode("ascii")
port = address[1]
hosts.append(
ResolveResult(
hostname=host,
host=resolved_host,
port=port,
family=family,
proto=0,
flags=_NUMERIC_SOCKET_FLAGS,
)
)

if not hosts:
raise OSError("DNS lookup failed")

return hosts

async def close(self) -> None:
"""Close the resolver."""
self._resolver.cancel()
1 change: 1 addition & 0 deletions homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

aiodhcpwatcher==1.0.0
aiodiscover==2.0.0
aiodns==3.2.0
aiohttp-fast-url-dispatcher==0.3.0
aiohttp-zlib-ng==0.3.1
aiohttp==3.9.3
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ classifiers = [
]
requires-python = ">=3.12.0"
dependencies = [
"aiodns==3.2.0",
"aiohttp==3.9.3",
"aiohttp_cors==0.7.0",
"aiohttp-fast-url-dispatcher==0.3.0",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
-c homeassistant/package_constraints.txt

# Home Assistant Core
aiodns==3.2.0
aiohttp==3.9.3
aiohttp_cors==0.7.0
aiohttp-fast-url-dispatcher==0.3.0
Expand Down

0 comments on commit f497c46

Please sign in to comment.