Skip to content

Commit c93f537

Browse files
committed
fix: surface an unreadable IPV6_V6ONLY at warning, document best-effort bring-up
A failed IPV6_V6ONLY read assumes dual-stack (returning False could loop rebuilds when the rebuilt socket's read also fails), but if the socket really were v6-only that skips a needed rebuild and strands an added IPv4 family, so log it at warning rather than debug. Document that interface bring-up is best-effort, and note the benign group leave on the rebuilt listen socket.
1 parent 37f6b1a commit c93f537

2 files changed

Lines changed: 15 additions & 8 deletions

File tree

src/zeroconf/_core.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -454,9 +454,11 @@ async def async_update_interfaces(
454454
interface set is unchanged this is a no-op (no sockets touched,
455455
nothing re-announced). The listen socket is rebuilt if the new set
456456
needs a different address family; unicast mode is fixed at
457-
construction. Concurrent calls are serialized. Re-announcement is
458-
best-effort: a registration that fails to re-announce is logged, not
459-
raised, so one failure cannot block the others.
457+
construction. Concurrent calls are serialized. Bringing up interfaces
458+
is best-effort: a requested interface that fails to bind, or fails to
459+
re-join after a rebuild, is logged rather than raised, and likewise a
460+
registration that fails to re-announce is logged so one failure cannot
461+
block the others.
460462
"""
461463
# Resolve against the retained config but only commit it after the
462464
# engine reconcile succeeds, so a failed reconcile leaves the stored

src/zeroconf/_engine.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ def _listen_socket_supports(
8080
try:
8181
supported = not listen_socket.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY)
8282
except OSError as exc:
83-
# Reading IPV6_V6ONLY can fail (Windows rejects it on some sockets;
84-
# other platforms shouldn't). Assume dual-stack so a read failure can't
85-
# abort the rescan; at worst this skips a rebuild the next reconcile
86-
# re-evaluates, consistent with make_wrapped_transport's fallback.
87-
log.debug("Unable to read IPV6_V6ONLY, assuming dual-stack: %s", exc)
83+
# Reading IPV6_V6ONLY essentially never fails on a valid AF_INET6
84+
# socket. Assume dual-stack rather than abort the rescan; returning
85+
# False instead could loop rebuilds if the rebuilt socket's read also
86+
# fails. Log at warning, not debug, because if the socket really were
87+
# v6-only this skips a needed rebuild and leaves an added IPv4 family
88+
# unreceivable, which is worth surfacing.
89+
log.warning("Unable to read IPV6_V6ONLY, assuming dual-stack: %s", exc)
8890
return supported
8991

9092

@@ -255,6 +257,9 @@ async def async_update_interfaces(
255257
# The shared listen / dual-use socket is not a per-interface
256258
# sender; leaving the group or closing it would break receive.
257259
continue
260+
# After a rebuild, listen_socket is the new socket, which this gone
261+
# interface never joined (its membership died with the old socket);
262+
# the leave is then a benign no-op that drop_multicast_member swallows.
258263
self._async_close_sender(wrapped, listen_socket)
259264

260265
added = False

0 commit comments

Comments
 (0)