Using IPVersion.All leads to OSError: [Errno 101] Network is unreachable logged
When using AsyncZeroconf(ip_version=IPVersion.All) this can lead to the following warning being logged:
WARNING Error with socket 66 (('::1', 5353, 0, 0))): [Errno 101] Network is unreachable
Traceback (most recent call last):
File "/usr/local/lib/python3.11/asyncio/selector_events.py", line 1196, in sendto
self._sock.sendto(data, addr)
OSError: [Errno 101] Network is unreachable
It seems that listening to the IPv6 loopback on it's own isn't problematic, but when trying to send to that socket, it leads to the above error. The problematic socket is created via get_all_addresses_v6(), which returns the loopback interface with the ::1 address (the full tuple being (('::1', 0, 0), 1)).
This then leads to a socket with the follow options created:
import socket
import struct
s = socket.socket(family=socket.AF_INET6, type=socket.SOCK_DGRAM)
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, True)
s.bind(('::2', 5353, 0, 0))
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, struct.pack('@I', 1))
When this socket then is used, the stack trace appears:
s.sendto(b"Hello", ('ff02::fb', 5353, 0, 0))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OSError: [Errno 101] Network is unreachable
The relevant option seem to be the IPv6 specific binding to the interface index IPV6_MULTICAST_IF, in this case 1 for the loopback interface.
The problem here really is the missing multicast support flag on the loopback interface
$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
E.g. a Ethernet interface has the flag:
2: eno2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
On IPv4 it is probably not a problem since the socket option IPV6_MULTICAST_IF to bind to a specific interface index is IPv6 specific.
I found that e.g. the Matter SDK's minimal mDNS implementation simply skips loopback (see https://github.com/project-chip/connectedhomeip/blob/v1.2.0.1/src/lib/dnssd/minimal_mdns/AddressPolicy_DefaultImpl.cpp#L41-L53).
However, it seems we can learn that from the interface flags instead. But it would require a change in the ifaddr library.
Looks like macos has MULTICAST on lo0
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
inet 127.0.0.1 netmask 0xff000000
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
nd6 options=201<PERFORMNUD,DAD>
linux 4.4.x
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:24830216 errors:0 dropped:0 overruns:0 frame:0
TX packets:24830216 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:3042660938 (2.8 GiB) TX bytes:3042660938 (2.8 GiB)
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
In HA we already explicitly exclude loopback
zc_args["interfaces"] = [
str(source_ip)
for source_ip in await network.async_get_enabled_source_ips(hass)
if not source_ip.is_loopback
and not (isinstance(source_ip, IPv6Address) and source_ip.is_global)
and not (
isinstance(source_ip, IPv6Address)
and zc_args["ip_version"] == IPVersion.V4Only
)
and not (
isinstance(source_ip, IPv4Address)
and zc_args["ip_version"] == IPVersion.V6Only
)
]
There is even a docstring that loopback doesn't work.
Someone might need it though so I think we can change InterfaceChoice.All to exclude loopback, and add InterfaceChoice.AllWithLoopback
There is a iflags, I think it would be the better indication.
Other interfaces might have that restriction too. E.g. a manually created dummy device:
sudo ip link add name loop1 type dummy
sudo ip addr add ::2 dev loop1
This would allow to filter the interfaces smarter on our end: https://github.com/pydron/ifaddr/pull/59
That would be better but realistically we don't have that information unless your PR gets merged