Coverage for aiocoap/util/asyncio/getaddrinfo_addrconfig.py: 77%

22 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-15 22:10 +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 # Still setting that -- it spares us the traffic of pointless requests. 

52 flags=socket.AI_ADDRCONFIG, 

53 ) 

54 

55 if any(a[0] not in (socket.AF_INET6, socket.AF_INET) for a in addrinfo): 

56 log.warning("Addresses outside of INET and INET6 families ignored.") 

57 

58 v6_addresses = ( 

59 sockaddr for (family, *_, sockaddr) in addrinfo if family == socket.AF_INET6 

60 ) 

61 # Dress them just as AI_V4MAPPED would do. Note that we can't do (ip, port) 

62 # instead of sockaddr b/c it's destructured already for the family check, 

63 # and at that time it may be a different AF with more members. 

64 v4_addresses = ( 

65 ("::ffff:" + sockaddr[0], sockaddr[1], 0, 0) 

66 for (family, *_, sockaddr) in addrinfo 

67 if family == socket.AF_INET 

68 ) 

69 

70 yielded = 0 

71 for ip, port, flowinfo, scope_id in itertools.chain(v6_addresses, v4_addresses): 

72 # Side-step connectivity test when explicitly giving an address. This 

73 # should be purely a cosmetic change, but given we try not to give IP 

74 # addresses special treatment over host names, it needs to be done 

75 # somewhere if we don't want connection attempts to explicit V6 

76 # addresses to err with "No address information found" on V4-only 

77 # hosts. 

78 if ip != host: 

79 with socket.socket( 

80 family=socket.AF_INET6, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP 

81 ) as tempsock: 

82 try: 

83 tempsock.connect((ip, port)) 

84 except OSError as e: 

85 if e.errno == errno.ENETUNREACH: 

86 continue 

87 

88 yield (ip, port, flowinfo, scope_id) 

89 yielded += 1 

90 

91 if not yielded: 

92 # That's the behavior of getaddrinfo -- not return empty (so we 

93 # shouldn't return empty just b/c everything was filtered either). Do 

94 # we need to be any more specific? The gaierror are caught later 

95 # anyway... 

96 raise socket.gaierror