@@ -189,9 +189,10 @@ async def async_update_interfaces(
189189
190190 Adds a per-interface responder socket for each interface that
191191 appeared and tears down the socket for each interface that
192- disappeared, diffing on the bound address. The shared listen
193- socket (including the Default single-family dual-use socket) is
194- never torn down here. Returns whether any responder socket was
192+ disappeared, diffing on the bound address. A Default single-family
193+ instance's dual-use listen/responder socket is converted to a pure
194+ listener when moving to an explicit set; otherwise the shared listen
195+ socket is left intact. Returns whether any responder socket was
195196 added, so the caller can skip re-announcing when nothing appeared.
196197 """
197198 assert self .loop is not None
@@ -201,30 +202,35 @@ async def async_update_interfaces(
201202 listen_transport = self ._listen_transport
202203 listen_socket = listen_transport .sock if listen_transport is not None else None
203204
204- # A Default single-family instance shares the listen socket as its
205- # only sender; adding per-interface senders alongside it would double
206- # every announcement. Switching interface kind at runtime is not
207- # supported, so raise (before any state changes) rather than
208- # double-send. The no-arg refresh of a Default instance keys to the
209- # listen socket and never reaches here.
205+ # The listen socket's family is fixed at construction, so a desired
206+ # interface of another family (e.g. an IPv6 interface added to an IPv4
207+ # instance) needs a fresh listen socket before senders are reconciled,
208+ # otherwise the current senders would be torn down with no replacements
209+ # bound.
210+ needs_rebuild = listen_socket is not None and any (
211+ not _listen_socket_supports (listen_socket , interface ) for interface in desired .values ()
212+ )
213+
214+ # A Default single-family instance shares the listen socket as its only
215+ # sender (the dual-use socket). Moving it to an explicit interface set
216+ # abandons that optimization: demote the socket so it stops responding
217+ # (otherwise it would double every announcement on the overlapping
218+ # interface) and rebuild it as a pure listener (its existing group
219+ # memberships would otherwise collide with the new per-interface joins).
220+ # Once demoted it no longer counts as a per-interface sender, so the
221+ # interface it served gets a fresh responder like any other. The no-arg
222+ # refresh of a Default instance leaves desired == {its interface} and so
223+ # neither demotes nor rebuilds.
210224 if listen_transport is not None and any (
211225 wrapped .transport is listen_transport .transport for wrapped in self .senders
212226 ):
213227 listen_key = listen_transport .interface_key
214228 if any (key != listen_key for key in desired ):
215- raise RuntimeError (
216- "Cannot change interfaces on a Default single-family Zeroconf instance; "
217- "recreate it to use an explicit interface set"
218- )
219-
220- # The listen socket's family is fixed at construction. If a desired
221- # interface cannot be joined on it (e.g. an IPv6 interface added to an
222- # IPv4 instance), rebuild the listen socket for the new family before
223- # reconciling senders, otherwise the current senders would be torn down
224- # with no replacements bound.
225- if listen_socket is not None and any (
226- not _listen_socket_supports (listen_socket , interface ) for interface in desired .values ()
227- ):
229+ self .senders = [w for w in self .senders if w .transport is not listen_transport .transport ]
230+ current = {wrapped .interface_key : wrapped for wrapped in self .senders }
231+ needs_rebuild = True
232+
233+ if needs_rebuild :
228234 await self ._async_rebuild_listen_socket (apple_p2p , desired , current )
229235 listen_transport = self ._listen_transport
230236 listen_socket = listen_transport .sock if listen_transport is not None else None
@@ -310,9 +316,9 @@ async def _async_rebuild_listen_socket(
310316 """Replace the listen socket with one whose family covers the desired set.
311317
312318 The listen socket's family is otherwise fixed at construction; this
313- lets an instance start receiving a newly added address family. Only
314- called when a desired interface cannot be joined on the current listen
315- socket. The replacement family is derived from the desired set (not the
319+ lets an instance start receiving a newly added address family, and is
320+ also used to convert a Default dual-use socket to a pure listener. The
321+ replacement family is derived from the desired set (not the
316322 requested ip_version, which an explicit list can contradict) so it
317323 always covers every desired interface and never needs an immediate
318324 re-rebuild. Interfaces that are staying are re-joined on the new socket,
0 commit comments