Coverage for aiocoap / util / asyncio / getaddrinfo_addrconfig.py: 75%
24 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 12:28 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 12:28 +0000
1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
2#
3# SPDX-License-Identifier: MIT
5"""Helper module around getaddrinfo shortcomings"""
7import socket
8import itertools
9import errno
12async def getaddrinfo_routechecked(loop, log, host, port):
13 """Variant of getaddrinfo that works like AI_ADDRCONFIG should probably work.
15 This is not only a workaround for 12377_, but goes beyond it by checking
16 routability. Even with 12377 fixed, AI_ADDRCONFIG would just avoid sending
17 AAAA requests (if no v6 addresses were available at all). It would still
18 not avoid filtering out responses that are just not routable -- the typical
19 example being that a host may be in a network with a ULA, so it needs to
20 send AAAA requests because they might resolve to the ULA, but if a global
21 address comes back, we still don't want that address passed to the
22 application.
24 The family and flags arguments are not accepted, because they are what is
25 being altered (and it'd be hard to tell which combinations would work). It
26 roughly behaves as if flags were set to AI_ADDRCONFIG | AI_V4MAPPED, and
27 family to AF_INET6. The type and protocol are fixed to SOCK_DGRAM /
28 IPPROTO_UDP, because only there we known that connect is side-effect free.
30 This function also differs from getaddrinfo in that it returns an
31 asynchronous iterator (we don't want to check addresses we don't care
32 about), and in that it only returns the sockaddr (because the rest is fixed
33 anyway).
35 .. _12377: https://sourceware.org/bugzilla/show_bug.cgi?id=12377
36 """
38 # As an implementation note, we can't go through the AI_V4MAPPED feature,
39 # because if we used that, we'd only get V4 responses if there are no V6
40 # addresses. As we deliberately want to produce results on V4 addresses
41 # when both are provided (conditional on the V6 ones not being routable),
42 # we need to convert -- and then the quick way to do things is to use
43 # AF_UNSPEC and distinguish later.
45 addrinfo = await loop.getaddrinfo(
46 host,
47 port,
48 family=socket.AF_UNSPEC,
49 type=socket.SOCK_DGRAM,
50 proto=socket.IPPROTO_UDP,
51 # Setting AI_ADDRCONFIG would spare us even sending AAAA requests on
52 # v4-only hosts, but causes trouble eg. when `::1` is actually
53 # reachable but AI_ADDRCONFIG says no because the main interface has
54 # IPv6 disabled. (Really disabled, as in
55 # `/proc/sys/net/ipv6/conf/eth0/disable_ipv6`, not just not having any
56 # but the link-local addresses -- but that's what is currently in
57 # Codeberg CI).
58 flags=0,
59 )
61 if any(a[0] not in (socket.AF_INET6, socket.AF_INET) for a in addrinfo):
62 log.warning("Addresses outside of INET and INET6 families ignored.")
64 log.debug("Original addrinfo set for %r:%s is %r", host, port, addrinfo)
66 v6_addresses = (
67 sockaddr for (family, *_, sockaddr) in addrinfo if family == socket.AF_INET6
68 )
69 # Dress them just as AI_V4MAPPED would do. Note that we can't do (ip, port)
70 # instead of sockaddr b/c it's destructured already for the family check,
71 # and at that time it may be a different AF with more members.
72 v4_addresses = (
73 ("::ffff:" + sockaddr[0], sockaddr[1], 0, 0)
74 for (family, *_, sockaddr) in addrinfo
75 if family == socket.AF_INET
76 )
78 yielded = 0
79 for ip, port, flowinfo, scope_id in itertools.chain(v6_addresses, v4_addresses):
80 # Side-step connectivity test when explicitly giving an address. This
81 # should be purely a cosmetic change, but given we try not to give IP
82 # addresses special treatment over host names, it needs to be done
83 # somewhere if we don't want connection attempts to explicit V6
84 # addresses to err with "No address information found" on V4-only
85 # hosts.
86 if ip != host:
87 with socket.socket(
88 family=socket.AF_INET6, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP
89 ) as tempsock:
90 try:
91 tempsock.connect((ip, port))
92 except OSError as e:
93 if e.errno == errno.ENETUNREACH:
94 log.debug(
95 "Not emitting address %s for name %r because it is unreachable (emulating AI_ADDRCONFIG)",
96 ip,
97 host,
98 )
99 continue
101 yield (ip, port, flowinfo, scope_id)
102 yielded += 1
104 if not yielded:
105 # That's the behavior of getaddrinfo -- not return empty (so we
106 # shouldn't return empty just b/c everything was filtered either). Do
107 # we need to be any more specific? The gaierror are caught later
108 # anyway...
109 raise socket.gaierror