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
« 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"""This module implements a MessageInterface for UDP based on a variation of
6the asyncio DatagramProtocol.
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.
15Configuration
16-------------
18This transport is configured in the ``transports.udp`` item of an
19:mod:`aiocoap.config`, a :class:`Udp6Parameters
20<aiocoap.config.Udp6Parameters>` instance.
22Implementation
23--------------
25This requires using some standardized and (as of 2025) widely supported
26features:
28* ``IPV6_JOIN_GROUP`` for multicast membership management.
30* ``IPV6_RECVPKTINFO`` to determine incoming packages' destination addresses
31 (was it multicast) and to return packages from the same address,
33* ``recvmsg`` to obtain that data (and, see below, also data from the error queue).
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.
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.
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"""
61import asyncio
62import contextvars
63import errno
64import os
65import socket
66import ipaddress
67import struct
68import weakref
69from collections import namedtuple
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
88"""The `struct in6_pktinfo` from RFC3542"""
89_in6_pktinfo = struct.Struct("16sI")
91_ipv6_unspecified = socket.inet_pton(socket.AF_INET6, "::")
92_ipv4_unspecified = socket.inet_pton(socket.AF_INET6, "::ffff:0.0.0.0")
95class InterfaceOnlyPktinfo(bytes):
96 """A thin wrapper over bytes that represent a pktinfo built just to select
97 an outgoing interface.
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)."""
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.
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.
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 """
134 def __init__(self, sockaddr, interface, *, pktinfo=None):
135 self.sockaddr = sockaddr
136 self.pktinfo = pktinfo
137 self._interface = weakref.ref(interface)
139 scheme = "coap"
141 interface = property(lambda self: self._interface())
143 # Unlike for other remotes, this is settable per instance.
144 maximum_block_size_exp = constants.MAX_REGULAR_BLOCK_SIZE_EXP
146 def __hash__(self):
147 return hash(self.sockaddr[:-1])
149 def __eq__(self, other):
150 return self.sockaddr[:-1] == other.sockaddr[:-1]
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 )
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.
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)
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."""
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
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)
201 return "%s%s" % (self._strip_v4mapped(addr), interface)
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."""
207 addr, interface = _in6_pktinfo.unpack_from(self.pktinfo)
209 return self._strip_v4mapped(addr)
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
219 @property
220 def hostinfo(self):
221 port = self.sockaddr[1]
222 if port == COAP_PORT:
223 port = None
225 # plainaddress: don't assume other applications can deal with v4mapped addresses
226 return hostportjoin(self._plainaddress(), port)
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)
238 @property
239 def uri_base(self):
240 return "coap://" + self.hostinfo
242 @property
243 def uri_base_local(self):
244 return "coap://" + self.hostinfo_local
246 @property
247 def is_multicast(self):
248 return ipaddress.ip_address(self._plainaddress().split("%", 1)[0]).is_multicast
250 @property
251 def is_multicast_locally(self):
252 return ipaddress.ip_address(self._plainaddress_local()).is_multicast
254 def as_response_address(self):
255 if not self.is_multicast_locally:
256 return self
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)
263 @property
264 def blockwise_key(self):
265 return (self.sockaddr, self.pktinfo)
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")
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))
281class MessageInterfaceUDP6(RecvmsgDatagramProtocol, interfaces.MessageInterface):
282 # Set between prepare_transport_endpoints and start_transport_endpoint
283 _ctx: interfaces.MessageManager
285 def __init__(self, bind, log, loop):
286 self.__bind = bind
287 self.log = log
288 self.loop = loop
290 self._shutting_down = (
291 None #: Future created and used in the .shutdown() method.
292 )
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``
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 )
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]
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.
324 After this, interconnect the instance with a message manager, and then
325 call start_transport_endpoint() to receive messages.
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."""
333 assert params.udp6 is not None, (
334 "Transport initiated without actual configuration"
335 )
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"]
346 reuseport = params.udp6.reuse_port
347 if reuseport is None:
348 reuseport = defaults.has_reuse_port()
350 for joined in bind:
351 (host, port) = hostportsplit(joined)
352 if port is None:
353 port = COAP_PORT
355 log.debug("Resolving address for %r (port %r) to bind to.", host, port)
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.
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)
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 )
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)
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")
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")
448 else:
449 raise RuntimeError("Unknown address format")
451 transport, protocol = await create_recvmsg_datagram_endpoint(
452 loop, lambda: cls(bind=address, log=log, loop=loop), sock=sock
453 )
455 await protocol.ready
457 yield protocol
458 except socket.gaierror:
459 raise error.ResolutionError(
460 "No local bindable address found for %s" % bind[0]
461 )
463 async def start_transport_endpoint(self):
464 """Run the last phase of transport startup: actually binding.
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)
474 async def shutdown(self):
475 self._shutting_down = asyncio.get_running_loop().create_future()
477 self.transport.close()
479 await self._shutting_down
481 del self._ctx
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)
500 async def recognize_remote(self, remote):
501 return isinstance(remote, UDP6EndpointAddress) and remote.interface == self
503 async def determine_remote(self, request):
504 if request.requested_scheme not in ("coap", None):
505 return None
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 )
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'.
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
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 )
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
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.
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
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
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
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
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
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
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)
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.
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
684 def error_received(self, exc):
685 """Implementation of the DatagramProtocol interface, called by the transport."""
687 remote = self._remote_being_sent_to.get()
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
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 )
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
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)
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)