Coverage for aiocoap / transports / udp6.py: 76%

294 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"""This module implements a MessageInterface for UDP based on a variation of 

6the asyncio DatagramProtocol. 

7 

8This implementation strives to be correct and complete behavior while still 

9only using a single socket (or a few, if distinct addresses are confgiured); 

10that is, to be usable for all kinds of multicast 

11traffic, to support server and client behavior at the same time, and to work 

12correctly even when multiple IPv6 and IPv4 (using V4MAPPED style addresses) 

13interfaces are present, and any of the interfaces has multiple addresses. 

14 

15Configuration 

16------------- 

17 

18This transport is configured in the ``transports.udp`` item of an 

19:mod:`aiocoap.config`, a :class:`Udp6Parameters 

20<aiocoap.config.Udp6Parameters>` instance. 

21 

22Implementation 

23-------------- 

24 

25This requires using some standardized and (as of 2025) widely supported 

26features: 

27 

28* ``IPV6_JOIN_GROUP`` for multicast membership management. 

29 

30* ``IPV6_RECVPKTINFO`` to determine incoming packages' destination addresses 

31 (was it multicast) and to return packages from the same address, 

32 

33* ``recvmsg`` to obtain that data (and, see below, also data from the error queue). 

34 

35The need for ``AI_V4MAPPED`` and ``AI_ADDRCONFIG`` is not manifest 

36in the code because the latter on its own is insufficient to enable seamless 

37interoperability with IPv4+IPv6 servers on IPv4-only hosts; instead, 

38short-lived sockets are created to assess which addresses are routable. (Those 

39are created in the :mod:`aiocoap.util.asyncio.getaddrinfo_addrconfig` module). 

40This should correctly deal with situations in which a client has an IPv6 ULA 

41assigned but no route, no matter whether the server advertises global IPv6 

42addresses or addresses inside that ULA. It can not deal with situations in 

43which the host has a default IPv6 route, but that route is not actually usable: 

44There is no Happy Eyeballs mechanism implemented. 

45 

46To the author's knowledge, there is no standardized mechanism for receiving 

47ICMP errors in such a setup. On Linux, ``IPV6_RECVERR`` and ``MSG_ERRQUEUE`` 

48are used to receive ICMP errors from the socket; on other platforms, a warning 

49is emitted that ICMP errors are ignored. Using a :mod:`.simple6` for clients is 

50recommended for those when working as a client only. 

51 

52Exceeding for the above error handling, no attempts are made to fall back to a 

53kind-of-correct or limited-functionality behavior if these options are 

54unavailable, for the resulting code would be hard to maintain ("``ifdef`` 

55hell") or would cause odd bugs at users (eg. servers that stop working when an 

56additional IPv6 address gets assigned). If the module does not work for you, 

57and the options can not be added easily to your platform, consider using the 

58:mod:`.simple6` module instead. 

59""" 

60 

61import asyncio 

62import contextvars 

63import errno 

64import os 

65import socket 

66import ipaddress 

67import struct 

68import weakref 

69from collections import namedtuple 

70 

71from ..config import TransportParameters 

72from ..message import Message, Direction 

73from ..numbers import constants 

74from .. import defaults 

75from .. import error 

76from .. import interfaces 

77from ..numbers import COAP_PORT 

78from ..util.asyncio.recvmsg import ( 

79 RecvmsgDatagramProtocol, 

80 create_recvmsg_datagram_endpoint, 

81) 

82from ..util.asyncio.getaddrinfo_addrconfig import ( 

83 getaddrinfo_routechecked as getaddrinfo, 

84) 

85from ..util import hostportjoin, hostportsplit 

86from ..util import socknumbers 

87 

88"""The `struct in6_pktinfo` from RFC3542""" 

89_in6_pktinfo = struct.Struct("16sI") 

90 

91_ipv6_unspecified = socket.inet_pton(socket.AF_INET6, "::") 

92_ipv4_unspecified = socket.inet_pton(socket.AF_INET6, "::ffff:0.0.0.0") 

93 

94 

95class InterfaceOnlyPktinfo(bytes): 

96 """A thin wrapper over bytes that represent a pktinfo built just to select 

97 an outgoing interface. 

98 

99 This must not be treated any different than a regular pktinfo, and is just 

100 tagged for better debug output. (Ie. if this is replaced everywhere with 

101 plain `bytes`, things must still work).""" 

102 

103 

104class UDP6EndpointAddress(interfaces.EndpointAddress): 

105 """Remote address type for :class:`MessageInterfaceUDP6`. Remote address is 

106 stored in form of a socket address; local address can be roundtripped by 

107 opaque pktinfo data. 

108 

109 For purposes of equality (and thus hashing), the local address is *not* 

110 checked. Neither is the scopeid that is part of the socket address. 

111 

112 >>> interface = type("FakeMessageInterface", (), {}) 

113 >>> if1_name = socket.if_indextoname(1) 

114 >>> local = UDP6EndpointAddress(socket.getaddrinfo('127.0.0.1', 5683, type=socket.SOCK_DGRAM, family=socket.AF_INET6, flags=socket.AI_V4MAPPED)[0][-1], interface) 

115 >>> local.is_multicast 

116 False 

117 >>> local.hostinfo 

118 '127.0.0.1' 

119 >>> all_coap_link1 = UDP6EndpointAddress(socket.getaddrinfo('ff02:0:0:0:0:0:0:fd%1', 1234, type=socket.SOCK_DGRAM, family=socket.AF_INET6)[0][-1], interface) 

120 >>> all_coap_link1.is_multicast 

121 True 

122 >>> all_coap_link1.hostinfo == '[ff02::fd%{}]:1234'.format(if1_name) 

123 True 

124 >>> all_coap_site = UDP6EndpointAddress(socket.getaddrinfo('ff05:0:0:0:0:0:0:fd', 1234, type=socket.SOCK_DGRAM, family=socket.AF_INET6)[0][-1], interface) 

125 >>> all_coap_site.is_multicast 

126 True 

127 >>> all_coap_site.hostinfo 

128 '[ff05::fd]:1234' 

129 >>> all_coap4 = UDP6EndpointAddress(socket.getaddrinfo('224.0.1.187', 5683, type=socket.SOCK_DGRAM, family=socket.AF_INET6, flags=socket.AI_V4MAPPED)[0][-1], interface) 

130 >>> all_coap4.is_multicast 

131 True 

132 """ 

133 

134 def __init__(self, sockaddr, interface, *, pktinfo=None): 

135 self.sockaddr = sockaddr 

136 self.pktinfo = pktinfo 

137 self._interface = weakref.ref(interface) 

138 

139 scheme = "coap" 

140 

141 interface = property(lambda self: self._interface()) 

142 

143 # Unlike for other remotes, this is settable per instance. 

144 maximum_block_size_exp = constants.MAX_REGULAR_BLOCK_SIZE_EXP 

145 

146 def __hash__(self): 

147 return hash(self.sockaddr[:-1]) 

148 

149 def __eq__(self, other): 

150 return self.sockaddr[:-1] == other.sockaddr[:-1] 

151 

152 def __repr__(self): 

153 return "<%s %s%s>" % ( 

154 type(self).__name__, 

155 self.hostinfo, 

156 " (locally %s)" % self._repr_pktinfo() if self.pktinfo is not None else "", 

157 ) 

158 

159 @staticmethod 

160 def _strip_v4mapped(address): 

161 """Turn anything that's a valid input to ipaddress.IPv6Address into a 

162 user-friendly string that's either an IPv6 or an IPv4 address. 

163 

164 This also compresses (normalizes) the IPv6 address as a convenient side 

165 effect.""" 

166 address = ipaddress.IPv6Address(address) 

167 mapped = address.ipv4_mapped 

168 if mapped is not None: 

169 return str(mapped) 

170 return str(address) 

171 

172 def _plainaddress(self): 

173 """Return the IP address part of the sockaddr in IPv4 notation if it is 

174 mapped, otherwise the plain v6 address including the interface 

175 identifier if set.""" 

176 

177 if self.sockaddr[3] != 0: 

178 try: 

179 scopepart = "%" + socket.if_indextoname(self.sockaddr[3]) 

180 except Exception: # could be an OS error, could just be that there is no function of this name, as it is on Android 

181 scopepart = "%" + str(self.sockaddr[3]) 

182 else: 

183 scopepart = "" 

184 if "%" in self.sockaddr[0]: 

185 # Fix for Python 3.6 and earlier that reported the scope information 

186 # in the IP literal (3.7 consistently expresses it in the tuple slot 3) 

187 scopepart = "" 

188 return self._strip_v4mapped(self.sockaddr[0]) + scopepart 

189 

190 def _repr_pktinfo(self): 

191 """What repr(self.pktinfo) would be if that were not a plain untyped bytestring""" 

192 addr, interface = _in6_pktinfo.unpack_from(self.pktinfo) 

193 if interface == 0: 

194 interface = "" 

195 else: 

196 try: 

197 interface = "%" + socket.if_indextoname(interface) 

198 except Exception as e: 

199 interface = "%%%d(%s)" % (interface, e) 

200 

201 return "%s%s" % (self._strip_v4mapped(addr), interface) 

202 

203 def _plainaddress_local(self): 

204 """Like _plainaddress, but on the address in the pktinfo. Unlike 

205 _plainaddress, this does not contain the interface identifier.""" 

206 

207 addr, interface = _in6_pktinfo.unpack_from(self.pktinfo) 

208 

209 return self._strip_v4mapped(addr) 

210 

211 @property 

212 def netif(self): 

213 """Textual interface identifier of the explicitly configured remote 

214 interface, or the interface identifier reported in an incoming 

215 link-local message. None if not set.""" 

216 index = self.sockaddr[3] 

217 return socket.if_indextoname(index) if index else None 

218 

219 @property 

220 def hostinfo(self): 

221 port = self.sockaddr[1] 

222 if port == COAP_PORT: 

223 port = None 

224 

225 # plainaddress: don't assume other applications can deal with v4mapped addresses 

226 return hostportjoin(self._plainaddress(), port) 

227 

228 @property 

229 def hostinfo_local(self): 

230 host = self._plainaddress_local() 

231 port = self.interface._local_port() 

232 if port == 0: 

233 raise ValueError("Local port read before socket has bound itself") 

234 if port == COAP_PORT: 

235 port = None 

236 return hostportjoin(host, port) 

237 

238 @property 

239 def uri_base(self): 

240 return "coap://" + self.hostinfo 

241 

242 @property 

243 def uri_base_local(self): 

244 return "coap://" + self.hostinfo_local 

245 

246 @property 

247 def is_multicast(self): 

248 return ipaddress.ip_address(self._plainaddress().split("%", 1)[0]).is_multicast 

249 

250 @property 

251 def is_multicast_locally(self): 

252 return ipaddress.ip_address(self._plainaddress_local()).is_multicast 

253 

254 def as_response_address(self): 

255 if not self.is_multicast_locally: 

256 return self 

257 

258 # Create a copy without pktinfo, as responses to messages received to 

259 # multicast addresses can not have their request's destination address 

260 # as source address 

261 return type(self)(self.sockaddr, self.interface) 

262 

263 @property 

264 def blockwise_key(self): 

265 return (self.sockaddr, self.pktinfo) 

266 

267 

268class SockExtendedErr( 

269 namedtuple( 

270 "_SockExtendedErr", "ee_errno ee_origin ee_type ee_code ee_pad ee_info ee_data" 

271 ) 

272): 

273 _struct = struct.Struct("IbbbbII") 

274 

275 @classmethod 

276 def load(cls, data): 

277 # unpack_from: recvmsg(2) says that more data may follow 

278 return cls(*cls._struct.unpack_from(data)) 

279 

280 

281class MessageInterfaceUDP6(RecvmsgDatagramProtocol, interfaces.MessageInterface): 

282 # Set between prepare_transport_endpoints and start_transport_endpoint 

283 _ctx: interfaces.MessageManager 

284 

285 def __init__(self, bind, log, loop): 

286 self.__bind = bind 

287 self.log = log 

288 self.loop = loop 

289 

290 self._shutting_down = ( 

291 None #: Future created and used in the .shutdown() method. 

292 ) 

293 

294 self.ready = asyncio.get_running_loop().create_future() #: Future that gets fulfilled by connection_made (ie. don't send before this is done; handled by ``create_..._context`` 

295 

296 # This is set while a send is underway to determine in the 

297 # error_received call site whom we were actually sending something to. 

298 # This is a workaround for the abysmal error handling unconnected 

299 # sockets have :-/ 

300 # 

301 # FIXME: Figure out whether aiocoap can at all support a context being 

302 # used with multiple aiocoap contexts, and if not, raise an error early 

303 # rather than just-in-case doing extra stuff here. 

304 self._remote_being_sent_to = contextvars.ContextVar( 

305 "_remote_being_sent_to", default=None 

306 ) 

307 

308 def _local_port(self): 

309 # FIXME: either raise an error if this is 0, or send a message to self 

310 # to force the OS to decide on a port. Right now, this reports wrong 

311 # results while the first message has not been sent yet. 

312 return self.transport.get_extra_info("socket").getsockname()[1] 

313 

314 @classmethod 

315 async def prepare_transport_endpoints( 

316 cls, 

317 *, 

318 params: TransportParameters, 

319 log, 

320 loop, 

321 ): 

322 """Produces instances that do *not* accept messages yet. 

323 

324 After this, interconnect the instance with a message manager, and then 

325 call start_transport_endpoint() to receive messages. 

326 

327 This split setup is needed because UDP sockets do not have TCP's 

328 separation between bind() and accept(); were this not split, there is a 

329 danger of receiving datagrams that are not trigger ICMP errors, but can 

330 still not be processed fully because the token and message manager are 

331 not fully set up.""" 

332 

333 assert params.udp6 is not None, ( 

334 "Transport initiated without actual configuration" 

335 ) 

336 

337 bind = params.udp6.bind 

338 if bind is None and params._legacy_bind is not None: 

339 bind = [hostportjoin(*params._legacy_bind)] 

340 if bind is None: 

341 if params.is_server: 

342 bind = ["[::]"] 

343 else: 

344 bind = ["[::]:0"] 

345 

346 reuseport = params.udp6.reuse_port 

347 if reuseport is None: 

348 reuseport = defaults.has_reuse_port() 

349 

350 for joined in bind: 

351 (host, port) = hostportsplit(joined) 

352 if port is None: 

353 port = COAP_PORT 

354 

355 log.debug("Resolving address for %r (port %r) to bind to.", host, port) 

356 

357 # Using getaddrinfo before bind for multiple reasons: 

358 # 

359 # * getaddrinfo might produce multiple addresses, typical example 

360 # is `localhost`. 

361 # 

362 # * bind does not populate the zone identifier of an IPv6 address 

363 # from a %-suffix, making it impossible without a getaddrinfo (or 

364 # manual parsing into the tuple) to bind to a specific link-local 

365 # address. 

366 try: 

367 async for address in getaddrinfo( 

368 loop, 

369 log, 

370 host, 

371 port, 

372 ): 

373 log.debug("Found address %r, preparing transport for it.", address) 

374 # FIXME: 

375 # 

376 # * Should we tolerate not being able to bind to all addresses, 

377 # eg. so that an anycast server can still use its name? 

378 # * If so, can we find out before we yield and later bind, or 

379 # do we need to back down later? 

380 # 

381 # For the time being the resolution is that setup will fail 

382 # if not all; users of anycast / DNS-switched domains 

383 # better have an "this-instance-only" name, or give 

384 # addresses explicitly. 

385 

386 sock = socket.socket(family=socket.AF_INET6, type=socket.SOCK_DGRAM) 

387 if reuseport: 

388 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 

389 sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) 

390 

391 try: 

392 sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_RECVPKTINFO, 1) 

393 except NameError: 

394 raise RuntimeError( 

395 "RFC3542 PKTINFO flags are unavailable, unable to create a udp6 transport." 

396 ) 

397 if socknumbers.HAS_RECVERR: 

398 sock.setsockopt( 

399 socket.IPPROTO_IPV6, socknumbers.IPV6_RECVERR, 1 

400 ) 

401 # i'm curious why this is required; didn't IPV6_V6ONLY=0 already make 

402 # it clear that i don't care about the ip version as long as everything looks the same? 

403 sock.setsockopt(socket.IPPROTO_IP, socknumbers.IP_RECVERR, 1) 

404 else: 

405 log.warning( 

406 "Transport udp6 set up on platform without RECVERR capability. ICMP errors will be ignored." 

407 ) 

408 

409 interface_string: str 

410 for address_string, interface_string in sum( 

411 map( 

412 # Expand shortcut of "interface name means default CoAP all-nodes addresses" 

413 lambda i: ( 

414 [(a, i) for a in constants.MCAST_ALL] 

415 if isinstance(i, str) 

416 else [i] 

417 ), 

418 params._legacy_multicast or [], 

419 ), 

420 [], 

421 ): 

422 mcaddress = ipaddress.ip_address(address_string) 

423 interface = socket.if_nametoindex(interface_string) 

424 

425 if isinstance(mcaddress, ipaddress.IPv4Address): 

426 s = struct.pack( 

427 "4s4si", 

428 mcaddress.packed, 

429 socket.inet_aton("0.0.0.0"), 

430 interface, 

431 ) 

432 try: 

433 sock.setsockopt( 

434 socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, s 

435 ) 

436 except OSError: 

437 log.warning("Could not join IPv4 multicast group") 

438 

439 elif isinstance(mcaddress, ipaddress.IPv6Address): 

440 s = struct.pack("16si", mcaddress.packed, interface) 

441 try: 

442 sock.setsockopt( 

443 socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, s 

444 ) 

445 except OSError: 

446 log.warning("Could not join IPv6 multicast group") 

447 

448 else: 

449 raise RuntimeError("Unknown address format") 

450 

451 transport, protocol = await create_recvmsg_datagram_endpoint( 

452 loop, lambda: cls(bind=address, log=log, loop=loop), sock=sock 

453 ) 

454 

455 await protocol.ready 

456 

457 yield protocol 

458 except socket.gaierror: 

459 raise error.ResolutionError( 

460 "No local bindable address found for %s" % bind[0] 

461 ) 

462 

463 async def start_transport_endpoint(self): 

464 """Run the last phase of transport startup: actually binding. 

465 

466 This is done in a 2nd phase for reasons outlined in 

467 :meth:`MessageInterfaceUDP6.prepare_transport_endpoints`.""" 

468 self.log.debug( 

469 "Starting transport endpoint %r binding to %r", self, self.__bind 

470 ) 

471 # Should we special-case '[::]:0' here and just not call? Does it make any difference? 

472 self.transport.get_extra_info("socket").bind(self.__bind) 

473 

474 async def shutdown(self): 

475 self._shutting_down = asyncio.get_running_loop().create_future() 

476 

477 self.transport.close() 

478 

479 await self._shutting_down 

480 

481 del self._ctx 

482 

483 def send(self, message): 

484 ancdata = [] 

485 if message.remote.pktinfo is not None: 

486 ancdata.append( 

487 (socket.IPPROTO_IPV6, socket.IPV6_PKTINFO, message.remote.pktinfo) 

488 ) 

489 assert self._remote_being_sent_to.get(None) is None, ( 

490 "udp6.MessageInterfaceUDP6.send was reentered in a single task" 

491 ) 

492 self._remote_being_sent_to.set(message.remote) 

493 try: 

494 self.transport.sendmsg( 

495 message.encode(), ancdata, 0, message.remote.sockaddr 

496 ) 

497 finally: 

498 self._remote_being_sent_to.set(None) 

499 

500 async def recognize_remote(self, remote): 

501 return isinstance(remote, UDP6EndpointAddress) and remote.interface == self 

502 

503 async def determine_remote(self, request): 

504 if request.requested_scheme not in ("coap", None): 

505 return None 

506 

507 if request.unresolved_remote is not None: 

508 host, port = hostportsplit(request.unresolved_remote) 

509 port = port or COAP_PORT 

510 elif request.opt.uri_host: 

511 host = request.opt.uri_host 

512 if host.startswith("[") and host.endswith("]"): 

513 host = host[1:-1] 

514 port = request.opt.uri_port or COAP_PORT 

515 else: 

516 raise ValueError( 

517 "No location found to send message to (neither in .opt.uri_host nor in .remote)" 

518 ) 

519 

520 # Take aside the zone identifier. While it can pass through getaddrinfo 

521 # in some situations (eg. 'fe80::1234%eth0' will give 'fe80::1234' 

522 # scope eth0, and similar for ff02:: addresses), in others (eg. ff05::) 

523 # it gives 'Name or service not known'. 

524 

525 if "%" in host: 

526 host, zone = host.split("%", 1) 

527 try: 

528 zone = socket.if_nametoindex(zone) 

529 except OSError: 

530 raise error.ResolutionError("Invalid zone identifier %s" % zone) 

531 else: 

532 zone = None 

533 

534 try: 

535 # Note that this is our special addrinfo that ensures there is a 

536 # route. 

537 ip, port, flowinfo, scopeid = await getaddrinfo( 

538 self.loop, 

539 self.log, 

540 host, 

541 port, 

542 ).__anext__() 

543 except socket.gaierror: 

544 raise error.ResolutionError( 

545 "No address information found for requests to %r" % host 

546 ) 

547 

548 if zone is not None: 

549 # Still trying to preserve the information returned (libc can't do 

550 # it as described at 

551 # <https://unix.stackexchange.com/questions/174767/ipv6-zone-id-in-etc-hosts>) 

552 # in case something sane does come out of that. 

553 if scopeid != 0 and scopeid != zone: 

554 self.log.warning( 

555 "Resolved address of %s came with zone ID %d whereas explicit ID %d takes precedence", 

556 host, 

557 scopeid, 

558 zone, 

559 ) 

560 scopeid = zone 

561 

562 # We could be done here and return UDP6EndpointAddress(the reassembled 

563 # sockaddr, self), but: 

564 # 

565 # Linux (unlike FreeBSD) takes the sockaddr's scope ID only for 

566 # link-local scopes (as per ipv6(7), and discards it otherwise. It does 

567 # need the information of the selected interface, though, in order to 

568 # pick the right outgoing interface. Thus, we provide it in the local 

569 # portion. 

570 

571 if scopeid: 

572 # "Any" does not include "even be it IPv4" -- the underlying family 

573 # unfortunately needs to be set, or Linux will refuse to send. 

574 if ipaddress.IPv6Address(ip).ipv4_mapped is None: 

575 local_source = _ipv6_unspecified 

576 else: 

577 local_source = _ipv4_unspecified 

578 local = InterfaceOnlyPktinfo(_in6_pktinfo.pack(local_source, scopeid)) 

579 else: 

580 local = None 

581 

582 sockaddr = ip, port, flowinfo, scopeid 

583 result = UDP6EndpointAddress(sockaddr, self, pktinfo=local) 

584 if request.remote.maximum_block_size_exp < result.maximum_block_size_exp: 

585 result.maximum_block_size_exp = request.remote.maximum_block_size_exp 

586 return result 

587 

588 # 

589 # implementing the typical DatagramProtocol interfaces. 

590 # 

591 # note from the documentation: we may rely on connection_made to be called 

592 # before datagram_received -- but sending immediately after context 

593 # creation will still fail 

594 

595 def connection_made(self, transport): 

596 """Implementation of the DatagramProtocol interface, called by the transport.""" 

597 self.ready.set_result(True) 

598 self.transport = transport 

599 

600 def datagram_msg_received(self, data, ancdata, flags, address): 

601 """Implementation of the RecvmsgDatagramProtocol interface, called by the transport.""" 

602 pktinfo = None 

603 for cmsg_level, cmsg_type, cmsg_data in ancdata: 

604 if cmsg_level == socket.IPPROTO_IPV6 and cmsg_type == socket.IPV6_PKTINFO: 

605 pktinfo = cmsg_data 

606 else: 

607 self.log.info( 

608 "Received unexpected ancillary data to recvmsg: level %d, type %d, data %r", 

609 cmsg_level, 

610 cmsg_type, 

611 cmsg_data, 

612 ) 

613 if pktinfo is None: 

614 self.log.warning( 

615 "Did not receive requested pktinfo ancdata on message from %s", address 

616 ) 

617 try: 

618 message = Message.decode( 

619 data, UDP6EndpointAddress(address, self, pktinfo=pktinfo) 

620 ) 

621 except error.UnparsableMessage: 

622 self.log.warning("Ignoring unparsable message from %s", address) 

623 return 

624 message.direction = Direction.INCOMING 

625 

626 try: 

627 self._ctx.dispatch_message(message) 

628 except BaseException as exc: 

629 # Catching here because util.asyncio.recvmsg inherits 

630 # _SelectorDatagramTransport's bad handling of callback errors; 

631 # this is the last time we have a log at hand. 

632 self.log.error( 

633 "Exception raised through dispatch_message: %s", exc, exc_info=exc 

634 ) 

635 raise 

636 

637 def datagram_errqueue_received(self, data, ancdata, flags, address): 

638 assert flags == socknumbers.MSG_ERRQUEUE, ( 

639 "Received non-error data through the errqueue" 

640 ) 

641 pktinfo = None 

642 errno_value = None 

643 for cmsg_level, cmsg_type, cmsg_data in ancdata: 

644 assert cmsg_level == socket.IPPROTO_IPV6, ( 

645 "Received non-IPv6 protocol through the errqueue" 

646 ) 

647 if cmsg_type == socknumbers.IPV6_RECVERR: 

648 extended_err = SockExtendedErr.load(cmsg_data) 

649 self.log.debug("Socket error received, details: %s", extended_err) 

650 errno_value = extended_err.ee_errno 

651 elif cmsg_level == socket.IPPROTO_IPV6 and cmsg_type == socket.IPV6_PKTINFO: 

652 pktinfo = cmsg_data 

653 else: 

654 self.log.info( 

655 "Received unexpected ancillary data to recvmsg errqueue: level %d, type %d, data %r", 

656 cmsg_level, 

657 cmsg_type, 

658 cmsg_data, 

659 ) 

660 remote = UDP6EndpointAddress(address, self, pktinfo=pktinfo) 

661 

662 # not trying to decode a message from data -- that works for 

663 # "connection refused", doesn't work for "no route to host", and 

664 # anyway, when an icmp error comes back, everything pending from that 

665 # port should err out. 

666 

667 try: 

668 text = os.strerror(errno_value) 

669 symbol = errno.errorcode.get(errno_value, None) 

670 symbol = "" if symbol is None else f"{symbol}, " 

671 self._ctx.dispatch_error( 

672 OSError(errno_value, f"{text} ({symbol}received through errqueue)"), 

673 remote, 

674 ) 

675 except BaseException as exc: 

676 # Catching here because util.asyncio.recvmsg inherits 

677 # _SelectorDatagramTransport's bad handling of callback errors; 

678 # this is the last time we have a log at hand. 

679 self.log.error( 

680 "Exception raised through dispatch_error: %s", exc, exc_info=exc 

681 ) 

682 raise 

683 

684 def error_received(self, exc): 

685 """Implementation of the DatagramProtocol interface, called by the transport.""" 

686 

687 remote = self._remote_being_sent_to.get() 

688 

689 if remote is None: 

690 self.log.info( 

691 "Error received in situation with no way to to determine which sending caused the error; this should be accompanied by an error in another code path: %s", 

692 exc, 

693 ) 

694 return 

695 

696 if exc.errno == errno.ENETUNREACH and self.__bind[0] != "::": 

697 self.log.warning( 

698 "Forwarding ENETUNREACH from a UDP6 connection that is not bound to [::]. Consider having a `[::]:0` binding at the start of the list to initiate requests as a client on both IP families." 

699 ) 

700 

701 try: 

702 self._ctx.dispatch_error(exc, remote) 

703 except BaseException as exc: 

704 # Catching here because util.asyncio.recvmsg inherits 

705 # _SelectorDatagramTransport's bad handling of callback errors; 

706 # this is the last time we have a log at hand. 

707 self.log.error( 

708 "Exception raised through dispatch_error: %s", exc, exc_info=exc 

709 ) 

710 raise 

711 

712 def connection_lost(self, exc): 

713 # TODO better error handling -- find out what can cause this at all 

714 # except for a shutdown 

715 if exc is not None: 

716 self.log.error("Connection lost: %s", exc) 

717 

718 if self._shutting_down is None: 

719 self.log.error("Connection loss was not expected.") 

720 else: 

721 self._shutting_down.set_result(None)