Skip to content

Commit 9819c84

Browse files
committed
fix: reject non-working socket configuration
For IPv6, a shared listener/responder socket is not really possible when sending to link-local IPv6 multicast addresses (ff02::/16): The kernel needs to know which interface to use for routing. On IPv4, this is historically a bit different, the kernel just uses what it deems the primary/best route interface based on the routing table. But for IPv6, a message is rejected by Linux with OSError no 99 "Cannot assign requested address" and OSError no 65 "No route to host" on macOS. This change rejects InterfaceChoice.Default IPv6 only or IPv6/IPv4 dual-stack configurations. As a further cleanup, move the socket options for sending multicast packets out of the common socket creation code. For listen only sockets those settings are not needed. Since we allow shared listener/responder sockets, with IPv4 only sockets now, the macOS error address in #392 is not a problem anymore. Actually, we would like to get an exception in case we get into this combination, so remove the explicit exception handling.
1 parent 754b787 commit 9819c84

File tree

3 files changed

+34
-35
lines changed

3 files changed

+34
-35
lines changed

src/zeroconf/_utils/net.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,8 @@ def normalize_interface_choice(
147147
result: list[str | tuple[tuple[str, int, int], int]] = []
148148
if choice is InterfaceChoice.Default:
149149
if ip_version != IPVersion.V4Only:
150-
# IPv6 multicast uses interface 0 to mean the default
151-
result.append((("", 0, 0), 0))
152-
if ip_version != IPVersion.V6Only:
153-
result.append("0.0.0.0")
150+
raise RuntimeError("`InterfaceChoice.Default` is only supported with IPv4.")
151+
result.append("0.0.0.0")
154152
elif choice is InterfaceChoice.All:
155153
if ip_version != IPVersion.V4Only:
156154
result.extend(get_all_addresses_v6())
@@ -198,28 +196,33 @@ def set_so_reuseport_if_available(s: socket.socket) -> None:
198196
raise
199197

200198

201-
def set_mdns_port_socket_options_for_ip_version(
199+
def set_respond_socket_multicast_options(
202200
s: socket.socket,
203-
bind_addr: tuple[str] | tuple[str, int, int],
204201
ip_version: IPVersion,
205202
) -> None:
206-
"""Set ttl/hops and loop for mdns port."""
207-
if ip_version != IPVersion.V6Only:
208-
ttl = struct.pack(b"B", 255)
209-
loop = struct.pack(b"B", 1)
203+
"""Set ttl/hops and loop for mDNS respond socket."""
204+
if ip_version == IPVersion.V4Only:
210205
# OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and
211206
# IP_MULTICAST_LOOP socket options as an unsigned char.
212-
try:
213-
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
214-
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop)
215-
except OSError as e:
216-
if bind_addr[0] != "" or get_errno(e) != errno.EINVAL: # Fails to set on MacOS
217-
raise
218-
219-
if ip_version != IPVersion.V4Only:
207+
ttl = struct.pack(b"B", 255)
208+
loop = struct.pack(b"B", 1)
209+
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
210+
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop)
211+
elif ip_version == IPVersion.V6Only:
220212
# However, char doesn't work here (at least on Linux)
221213
s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)
222214
s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, True)
215+
else:
216+
# A shared sender socket is not really possible, especially with link-local
217+
# multicast addresses (ff02::/16), the kernel needs to know which interface
218+
# to use for routing.
219+
#
220+
# It seems that macOS even refuses to take IPv4 socket options if this is an
221+
# AF_INET6 socket.
222+
#
223+
# In theory we could reconfigure the socket on each send, but that is not
224+
# really practical for Python Zerconf.
225+
raise RuntimeError("Dual-stack responder socket not supported")
223226

224227

225228
def new_socket(
@@ -244,9 +247,6 @@ def new_socket(
244247
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
245248
set_so_reuseport_if_available(s)
246249

247-
if port == _MDNS_PORT:
248-
set_mdns_port_socket_options_for_ip_version(s, bind_addr, ip_version)
249-
250250
if apple_p2p:
251251
# SO_RECV_ANYIF = 0x1104
252252
# https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/socket.h
@@ -402,6 +402,7 @@ def new_respond_socket(
402402
socket.IP_MULTICAST_IF,
403403
socket.inet_aton(cast(str, interface)),
404404
)
405+
set_respond_socket_multicast_options(respond_socket, IPVersion.V6Only if is_v6 else IPVersion.V4Only)
405406
return respond_socket
406407

407408

@@ -423,6 +424,8 @@ def create_sockets(
423424
if not unicast and interfaces is InterfaceChoice.Default:
424425
for interface in normalized_interfaces:
425426
add_multicast_member(cast(socket.socket, listen_socket), interface)
427+
# Sent responder socket options to the dual-use listen socket
428+
set_respond_socket_multicast_options(cast(socket.socket, listen_socket), ip_version)
426429
return listen_socket, [cast(socket.socket, listen_socket)]
427430

428431
respond_sockets = []

tests/test_core.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,16 @@ def test_close_multiple_times(self):
8787
def test_launch_and_close_v4_v6(self):
8888
rv = r.Zeroconf(interfaces=r.InterfaceChoice.All, ip_version=r.IPVersion.All)
8989
rv.close()
90-
rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.All)
91-
rv.close()
90+
with pytest.raises(RuntimeError):
91+
r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.All)
9292

9393
@unittest.skipIf(not has_working_ipv6(), "Requires IPv6")
9494
@unittest.skipIf(os.environ.get("SKIP_IPV6"), "IPv6 tests disabled")
9595
def test_launch_and_close_v6_only(self):
9696
rv = r.Zeroconf(interfaces=r.InterfaceChoice.All, ip_version=r.IPVersion.V6Only)
9797
rv.close()
98-
rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.V6Only)
99-
rv.close()
98+
with pytest.raises(RuntimeError):
99+
r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.V6Only)
100100

101101
@unittest.skipIf(sys.platform == "darwin", reason="apple_p2p failure path not testable on mac")
102102
def test_launch_and_close_apple_p2p_not_mac(self):

tests/utils/test_net.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,23 +162,19 @@ def test_set_so_reuseport_if_available_not_present():
162162
netutils.set_so_reuseport_if_available(sock)
163163

164164

165-
def test_set_mdns_port_socket_options_for_ip_version():
165+
def test_set_respond_socket_multicast_options():
166166
"""Test OSError with errno with EINVAL and bind address ''.
167167
168168
from setsockopt IP_MULTICAST_TTL does not raise."""
169169
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
170170

171-
# Should raise on EPERM always
172-
with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError(errno.EPERM, None)):
173-
netutils.set_mdns_port_socket_options_for_ip_version(sock, ("",), r.IPVersion.V4Only)
174-
175-
# Should raise on EINVAL always when bind address is not ''
171+
# Should raise on EINVAL always
176172
with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)):
177-
netutils.set_mdns_port_socket_options_for_ip_version(sock, ("127.0.0.1",), r.IPVersion.V4Only)
173+
netutils.set_respond_socket_multicast_options(sock, r.IPVersion.V4Only)
178174

179-
# Should not raise on EINVAL when bind address is ''
180-
with patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)):
181-
netutils.set_mdns_port_socket_options_for_ip_version(sock, ("",), r.IPVersion.V4Only)
175+
# Should raise on EINVAL always
176+
with pytest.raises(RuntimeError):
177+
netutils.set_respond_socket_multicast_options(sock, r.IPVersion.All)
182178

183179

184180
def test_add_multicast_member(caplog: pytest.LogCaptureFixture) -> None:

0 commit comments

Comments
 (0)