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

1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors 

2# 

3# SPDX-License-Identifier: MIT 

4 

5"""Helper module around getaddrinfo shortcomings""" 

6 

7import socket 

8import itertools 

9import errno 

10 

11 

12async def getaddrinfo_routechecked(loop, log, host, port): 

13 """Variant of getaddrinfo that works like AI_ADDRCONFIG should probably work. 

14 

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. 

23 

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. 

29 

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). 

34 

35 .. _12377: https://sourceware.org/bugzilla/show_bug.cgi?id=12377 

36 """ 

37 

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. 

44 

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 ) 

60 

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.") 

63 

64 log.debug("Original addrinfo set for %r:%s is %r", host, port, addrinfo) 

65 

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 ) 

77 

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 

100 

101 yield (ip, port, flowinfo, scope_id) 

102 yielded += 1 

103 

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