fix: address non-working socket configuration (#1563) · python-zeroconf/python-zeroconf@cc0f835

@@ -168,8 +168,17 @@ def normalize_interface_choice(

168168

result: list[str | tuple[tuple[str, int, int], int]] = []

169169

if choice is InterfaceChoice.Default:

170170

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

173182

if ip_version != IPVersion.V6Only:

174183

result.append("0.0.0.0")

175184

elif choice is InterfaceChoice.All:

@@ -220,28 +229,33 @@ def set_so_reuseport_if_available(s: socket.socket) -> None:

220229

raise

221230222231223-

def set_mdns_port_socket_options_for_ip_version(

232+

def set_respond_socket_multicast_options(

224233

s: socket.socket,

225-

bind_addr: tuple[str] | tuple[str, int, int],

226234

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

243246

s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)

244247

s.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")

245259246260247261

def new_socket(

@@ -266,14 +280,12 @@ def new_socket(

266280

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

267281

set_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-272283

if apple_p2p:

273284

# SO_RECV_ANYIF = 0x1104

274285

# https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/socket.h

275286

s.setsockopt(socket.SOL_SOCKET, 0x1104, 1)

276287288+

# Bind expects (address, port) for AF_INET and (address, port, flowinfo, scope_id) for AF_INET6

277289

bind_tup = (bind_addr[0], port, *bind_addr[1:])

278290

try:

279291

s.bind(bind_tup)

@@ -392,15 +404,27 @@ def add_multicast_member(

392404

def new_respond_socket(

393405

interface: str | tuple[tuple[str, int, int], int],

394406

apple_p2p: bool = False,

407+

unicast: bool = False,

395408

) -> socket.socket | None:

409+

"""Create interface specific socket for responding to multicast queries."""

396410

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

397416

respond_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,

398419

ip_version=(IPVersion.V6Only if is_v6 else IPVersion.V4Only),

399420

apple_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+402425

if not respond_socket:

403426

return None

427+404428

log.debug("Configuring socket %s with multicast interface %s", respond_socket, interface)

405429

if is_v6:

406430

iface_bin = struct.pack("@I", cast(int, interface[1]))

@@ -411,6 +435,7 @@ def new_respond_socket(

411435

socket.IP_MULTICAST_IF,

412436

socket.inet_aton(cast(str, interface)),

413437

)

438+

set_respond_socket_multicast_options(respond_socket, IPVersion.V6Only if is_v6 else IPVersion.V4Only)

414439

return respond_socket

415440416441

@@ -423,33 +448,27 @@ def create_sockets(

423448

if unicast:

424449

listen_socket = None

425450

else:

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)

427452428453

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

435462

return listen_socket, [cast(socket.socket, listen_socket)]

436463437464

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

453472454473

if respond_socket is not None:

455474

respond_sockets.append(respond_socket)