python-zeroconf icon indicating copy to clipboard operation
python-zeroconf copied to clipboard

Using IPVersion.All leads to OSError: [Errno 101] Network is unreachable logged

Open agners opened this issue 2 years ago • 10 comments

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.

agners avatar Feb 07 '24 18:02 agners

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.

agners avatar Feb 07 '24 19:02 agners

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>

bdraco avatar Feb 07 '24 21:02 bdraco

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)

bdraco avatar Feb 07 '24 21:02 bdraco

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

bdraco avatar Feb 07 '24 21:02 bdraco

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

bdraco avatar Feb 07 '24 21:02 bdraco

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

bdraco avatar Feb 07 '24 22:02 bdraco

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

agners avatar Feb 07 '24 22:02 agners

This would allow to filter the interfaces smarter on our end: https://github.com/pydron/ifaddr/pull/59

agners avatar Feb 07 '24 22:02 agners

That would be better but realistically we don't have that information unless your PR gets merged

bdraco avatar Feb 07 '24 22:02 bdraco