fix: address non-working socket configuration (#1563) · python-zeroconf/python-zeroconf@cc0f835
@@ -168,8 +168,17 @@ def normalize_interface_choice(
168168result: list[str | tuple[tuple[str, int, int], int]] = []
169169if choice is InterfaceChoice.Default:
170170if ip_version != IPVersion.V4Only:
171-# IPv6 multicast uses interface 0 to mean the default
172-result.append((("", 0, 0), 0))
171+# IPv6 multicast uses interface 0 to mean the default. However,
172+# the default interface can't be used for outgoing IPv6 multicast
173+# requests. In a way, interface choice default isn't really working
174+# with IPv6. Inform the user accordingly.
175+message = (
176+"IPv6 multicast requests can't be sent using default interface. "
177+"Use V4Only, InterfaceChoice.All or an explicit list of interfaces."
178+ )
179+log.error(message)
180+warnings.warn(message, DeprecationWarning, stacklevel=2)
181+result.append((("::", 0, 0), 0))
173182if ip_version != IPVersion.V6Only:
174183result.append("0.0.0.0")
175184elif choice is InterfaceChoice.All:
@@ -220,28 +229,33 @@ def set_so_reuseport_if_available(s: socket.socket) -> None:
220229raise
221230222231223-def set_mdns_port_socket_options_for_ip_version(
232+def set_respond_socket_multicast_options(
224233s: socket.socket,
225-bind_addr: tuple[str] | tuple[str, int, int],
226234ip_version: IPVersion,
227235) -> None:
228-"""Set ttl/hops and loop for mdns port."""
229-if ip_version != IPVersion.V6Only:
230-ttl = struct.pack(b"B", 255)
231-loop = struct.pack(b"B", 1)
236+"""Set ttl/hops and loop for mDNS respond socket."""
237+if ip_version == IPVersion.V4Only:
232238# OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and
233239# IP_MULTICAST_LOOP socket options as an unsigned char.
234-try:
235-s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
236-s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop)
237-except OSError as e:
238-if bind_addr[0] != "" or get_errno(e) != errno.EINVAL: # Fails to set on MacOS
239-raise
240-241-if ip_version != IPVersion.V4Only:
240+ttl = struct.pack(b"B", 255)
241+loop = struct.pack(b"B", 1)
242+s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
243+s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop)
244+elif ip_version == IPVersion.V6Only:
242245# However, char doesn't work here (at least on Linux)
243246s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)
244247s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, True)
248+else:
249+# A shared sender socket is not really possible, especially with link-local
250+# multicast addresses (ff02::/16), the kernel needs to know which interface
251+# to use for routing.
252+#
253+# It seems that macOS even refuses to take IPv4 socket options if this is an
254+# AF_INET6 socket.
255+#
256+# In theory we could reconfigure the socket on each send, but that is not
257+# really practical for Python Zerconf.
258+raise RuntimeError("Dual-stack responder socket not supported")
245259246260247261def new_socket(
@@ -266,14 +280,12 @@ def new_socket(
266280s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
267281set_so_reuseport_if_available(s)
268282269-if port == _MDNS_PORT:
270-set_mdns_port_socket_options_for_ip_version(s, bind_addr, ip_version)
271-272283if apple_p2p:
273284# SO_RECV_ANYIF = 0x1104
274285# https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/socket.h
275286s.setsockopt(socket.SOL_SOCKET, 0x1104, 1)
276287288+# Bind expects (address, port) for AF_INET and (address, port, flowinfo, scope_id) for AF_INET6
277289bind_tup = (bind_addr[0], port, *bind_addr[1:])
278290try:
279291s.bind(bind_tup)
@@ -392,15 +404,27 @@ def add_multicast_member(
392404def new_respond_socket(
393405interface: str | tuple[tuple[str, int, int], int],
394406apple_p2p: bool = False,
407+unicast: bool = False,
395408) -> socket.socket | None:
409+"""Create interface specific socket for responding to multicast queries."""
396410is_v6 = isinstance(interface, tuple)
411+412+# For response sockets:
413+# - Bind explicitly to the interface address
414+# - Use ephemeral ports if in unicast mode
415+# - Create socket according to the interface IP type (IPv4 or IPv6)
397416respond_socket = new_socket(
417+bind_addr=cast(tuple[tuple[str, int, int], int], interface)[0] if is_v6 else (cast(str, interface),),
418+port=0 if unicast else _MDNS_PORT,
398419ip_version=(IPVersion.V6Only if is_v6 else IPVersion.V4Only),
399420apple_p2p=apple_p2p,
400-bind_addr=cast(tuple[tuple[str, int, int], int], interface)[0] if is_v6 else (cast(str, interface),),
401421 )
422+if unicast:
423+return respond_socket
424+402425if not respond_socket:
403426return None
427+404428log.debug("Configuring socket %s with multicast interface %s", respond_socket, interface)
405429if is_v6:
406430iface_bin = struct.pack("@I", cast(int, interface[1]))
@@ -411,6 +435,7 @@ def new_respond_socket(
411435socket.IP_MULTICAST_IF,
412436socket.inet_aton(cast(str, interface)),
413437 )
438+set_respond_socket_multicast_options(respond_socket, IPVersion.V6Only if is_v6 else IPVersion.V4Only)
414439return respond_socket
415440416441@@ -423,33 +448,27 @@ def create_sockets(
423448if unicast:
424449listen_socket = None
425450else:
426-listen_socket = new_socket(ip_version=ip_version, apple_p2p=apple_p2p, bind_addr=("",))
451+listen_socket = new_socket(bind_addr=("",), ip_version=ip_version, apple_p2p=apple_p2p)
427452428453normalized_interfaces = normalize_interface_choice(interfaces, ip_version)
429454430-# If we are using InterfaceChoice.Default we can use
455+# If we are using InterfaceChoice.Default with only IPv4 or only IPv6, we can use
431456# a single socket to listen and respond.
432-if not unicast and interfaces is InterfaceChoice.Default:
433-for i in normalized_interfaces:
434-add_multicast_member(cast(socket.socket, listen_socket), i)
457+if not unicast and interfaces is InterfaceChoice.Default and ip_version != IPVersion.All:
458+for interface in normalized_interfaces:
459+add_multicast_member(cast(socket.socket, listen_socket), interface)
460+# Sent responder socket options to the dual-use listen socket
461+set_respond_socket_multicast_options(cast(socket.socket, listen_socket), ip_version)
435462return listen_socket, [cast(socket.socket, listen_socket)]
436463437464respond_sockets = []
438465439-for i in normalized_interfaces:
440-if not unicast:
441-if add_multicast_member(cast(socket.socket, listen_socket), i):
442-respond_socket = new_respond_socket(i, apple_p2p=apple_p2p)
443-else:
444-respond_socket = None
445-else:
446-is_v6 = isinstance(i, tuple)
447-respond_socket = new_socket(
448-port=0,
449-ip_version=IPVersion.V6Only if is_v6 else IPVersion.V4Only,
450-apple_p2p=apple_p2p,
451-bind_addr=cast(tuple[tuple[str, int, int], int], i)[0] if is_v6 else (cast(str, i),),
452- )
466+for interface in normalized_interfaces:
467+# Only create response socket if unicast or becoming multicast member was successful
468+if not unicast and not add_multicast_member(cast(socket.socket, listen_socket), interface):
469+continue
470+471+respond_socket = new_respond_socket(interface, apple_p2p=apple_p2p, unicast=unicast)
453472454473if respond_socket is not None:
455474respond_sockets.append(respond_socket)