@@ -168,8 +168,17 @@ def normalize_interface_choice(
168
168
result : list [str | tuple [tuple [str , int , int ], int ]] = []
169
169
if choice is InterfaceChoice .Default :
170
170
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 ))
173
182
if ip_version != IPVersion .V6Only :
174
183
result .append ("0.0.0.0" )
175
184
elif choice is InterfaceChoice .All :
@@ -220,28 +229,33 @@ def set_so_reuseport_if_available(s: socket.socket) -> None:
220
229
raise
221
230
222
231
223
- def set_mdns_port_socket_options_for_ip_version (
232
+ def set_respond_socket_multicast_options (
224
233
s : socket .socket ,
225
- bind_addr : tuple [str ] | tuple [str , int , int ],
226
234
ip_version : IPVersion ,
227
235
) -> 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 :
232
238
# OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and
233
239
# 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 :
242
245
# However, char doesn't work here (at least on Linux)
243
246
s .setsockopt (_IPPROTO_IPV6, socket .IPV6_MULTICAST_HOPS , 255 )
244
247
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" )
245
259
246
260
247
261
def new_socket (
@@ -266,14 +280,12 @@ def new_socket(
266
280
s .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
267
281
set_so_reuseport_if_available (s )
268
282
269
- if port == _MDNS_PORT :
270
- set_mdns_port_socket_options_for_ip_version (s , bind_addr , ip_version )
271
-
272
283
if apple_p2p :
273
284
# SO_RECV_ANYIF = 0x1104
274
285
# https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/socket.h
275
286
s .setsockopt (socket .SOL_SOCKET , 0x1104 , 1 )
276
287
288
+ # Bind expects (address, port) for AF_INET and (address, port, flowinfo, scope_id) for AF_INET6
277
289
bind_tup = (bind_addr [0 ], port , * bind_addr [1 :])
278
290
try :
279
291
s .bind (bind_tup )
@@ -392,15 +404,27 @@ def add_multicast_member(
392
404
def new_respond_socket (
393
405
interface : str | tuple [tuple [str , int , int ], int ],
394
406
apple_p2p : bool = False ,
407
+ unicast : bool = False ,
395
408
) -> socket .socket | None :
409
+ """Create interface specific socket for responding to multicast queries."""
396
410
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)
397
416
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 ,
398
419
ip_version = (IPVersion .V6Only if is_v6 else IPVersion .V4Only ),
399
420
apple_p2p = apple_p2p ,
400
- bind_addr = cast (tuple [tuple [str , int , int ], int ], interface )[0 ] if is_v6 else (cast (str , interface ),),
401
421
)
422
+ if unicast :
423
+ return respond_socket
424
+
402
425
if not respond_socket :
403
426
return None
427
+
404
428
log .debug ("Configuring socket %s with multicast interface %s" , respond_socket , interface )
405
429
if is_v6 :
406
430
iface_bin = struct .pack ("@I" , cast (int , interface [1 ]))
@@ -411,6 +435,7 @@ def new_respond_socket(
411
435
socket .IP_MULTICAST_IF ,
412
436
socket .inet_aton (cast (str , interface )),
413
437
)
438
+ set_respond_socket_multicast_options (respond_socket , IPVersion .V6Only if is_v6 else IPVersion .V4Only )
414
439
return respond_socket
415
440
416
441
@@ -423,33 +448,27 @@ def create_sockets(
423
448
if unicast :
424
449
listen_socket = None
425
450
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 )
427
452
428
453
normalized_interfaces = normalize_interface_choice (interfaces , ip_version )
429
454
430
- # 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
431
456
# 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 )
435
462
return listen_socket , [cast (socket .socket , listen_socket )]
436
463
437
464
respond_sockets = []
438
465
439
- 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 )
453
472
454
473
if respond_socket is not None :
455
474
respond_sockets .append (respond_socket )
0 commit comments