feat: add multicast_addresses to bind/multicast independently#1767
feat: add multicast_addresses to bind/multicast independently#1767bluetoothbot wants to merge 6 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1767 +/- ##
=======================================
Coverage 99.77% 99.77%
=======================================
Files 33 33
Lines 3536 3562 +26
Branches 498 508 +10
=======================================
+ Hits 3528 3554 +26
Misses 5 5
Partials 3 3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds a multicast_addresses escape hatch to let callers join additional multicast groups on the listen socket without also binding per-interface respond sockets—targeting sandboxed environments (e.g. iOS) where binding :5353 on physical interfaces can be blocked while multicast membership is still allowed/needed.
Changes:
- Extend
create_sockets()withmulticast_addressesand rejectmulticast_addresseswithunicast=True. - Thread the new parameter through
ZeroconfandAsyncZeroconfconstructors (docs + typing included). - Add unit tests for IPv4/IPv6 membership behavior, the InterfaceChoice.Default fast path, unicast rejection, and Zeroconf-level wiring.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/zeroconf/_utils/net.py |
Adds multicast_addresses handling in create_sockets() to join extra multicast memberships on the listen socket. |
src/zeroconf/_core.py |
Exposes/forwards multicast_addresses from Zeroconf.__init__() into socket creation and documents behavior. |
src/zeroconf/asyncio.py |
Exposes/forwards multicast_addresses from AsyncZeroconf.__init__() into Zeroconf. |
tests/utils/test_net.py |
Adds focused socket-creation tests covering IPv4/IPv6, default fast path, and unicast incompatibility. |
tests/test_core.py |
Adds a wiring test that verifies multicast_addresses is forwarded to create_sockets(). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| listen_socket, respond_sockets = create_sockets( | ||
| interfaces, | ||
| unicast, | ||
| ip_version, | ||
| apple_p2p=apple_p2p, | ||
| multicast_addresses=multicast_addresses, | ||
| ) |
| if not unicast and interfaces is InterfaceChoice.Default and ip_version != IPVersion.All: | ||
| for interface in normalized_interfaces: | ||
| add_multicast_member(cast(socket.socket, listen_socket), interface) | ||
| for interface in extra_multicast_members: | ||
| add_multicast_member(cast(socket.socket, listen_socket), interface) |
| zc = r.Zeroconf( | ||
| interfaces=["127.0.0.1"], | ||
| multicast_addresses=["192.168.1.5"], | ||
| unicast=True, |
| normalized_interfaces = normalize_interface_choice(interfaces, ip_version) | ||
| extra_multicast_members = ( | ||
| normalize_interface_choice(list(multicast_addresses), ip_version) if multicast_addresses else [] | ||
| ) |
PR Review — feat: add multicast_addresses to bind/multicast independentlyThe feature itself is a clean, additive escape hatch with sensible behaviour: extras are validated up-front against 🟡 Important1. Tuple entries silently dropped — type signature claims support, runtime ignores them (`src/zeroconf/_utils/net.py`, L469-475)
Follow the chain:
Net effect: Two options: either (a) extend 2. Unrelated TC-bit timing/cleanup-window change bundled into this PR (`tests/test_core.py`, L702-741)The diff to This matters because:
Please split this hunk out into its own follow-up PR with a 🟢 Suggestions1. Extract helpers to address @bdraco's DRY request (`src/zeroconf/_utils/net.py`, L455-520)@bdraco asked for this to be made more DRY. The Consider: def _validate_multicast_addresses(
multicast_addresses: Sequence[...] | None, ip_version: IPVersion, unicast: bool,
) -> None:
if not multicast_addresses:
return
if unicast:
raise ValueError("multicast_addresses is incompatible with unicast=True")
if ip_version == IPVersion.V4Only and any(_entry_ip_version(e) == 6 for e in multicast_addresses):
raise ValueError("multicast_addresses contains IPv6 entries but ip_version is V4Only")
if ip_version == IPVersion.V6Only and any(_entry_ip_version(e) == 4 for e in multicast_addresses):
raise ValueError("multicast_addresses contains IPv4 entries but ip_version is V6Only")
def _resolve_extra_multicast_members(
multicast_addresses: Sequence[...] | None,
normalized_interfaces: list[...],
ip_version: IPVersion,
) -> list[...]:
if not multicast_addresses:
return []
extras = normalize_interface_choice(list(multicast_addresses), ip_version)
interface_set = set(normalized_interfaces)
return [m for m in extras if m not in interface_set]
def _join_extra_multicast_members(listen_socket, extras):
for member in extras:
add_multicast_member(listen_socket, member)That collapses Checklist
SummaryThe feature itself is a clean, additive escape hatch with sensible behaviour: extras are validated up-front against To rebase specific severity levels, mention me: Automated review by Kōanf265d95 |
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
bdraco
left a comment
There was a problem hiding this comment.
Need to complete coverage
src/zeroconf/_utils/net.py 96.66% 0 Missing and 1 partia
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
371be53 to
4e4cf47
Compare
Rebase with requested adjustmentsBranch StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
4e4cf47 to
73e0af3
Compare
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
73e0af3 to
e7bc212
Compare
bdraco
left a comment
There was a problem hiding this comment.
coverage is incomplete
| Files with missing lines | Patch % | Lines |
|---|---|---|
| src/zeroconf/_utils/net.py | 93.54% | 1 Missing and 1 partial |
Files with missing lines Patch % Lines
src/zeroconf/_utils/net.py 93.54% 1 Missing and 1 partial
iOS and other sandboxed platforms allow joining the mDNS multicast group on a physical interface but forbid binding port 5353 on it because the system mDNS daemon owns the port. The existing ``interfaces`` parameter conflates the two: every entry both joins the multicast group and gets a respond socket bound to that interface's address. Add an optional ``multicast_addresses`` parameter on ``Zeroconf`` / ``AsyncZeroconf`` / ``create_sockets`` for additional addresses that are joined to the listen socket's multicast group without creating a respond socket. The default behavior is unchanged. Fixes python-zeroconf#1520
…up against interfaces
… validation and autodetect
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
e7bc212 to
56f00c5
Compare
| normalized_interfaces = normalize_interface_choice(interfaces, ip_version) | ||
| if multicast_addresses: | ||
| extra_multicast_members = normalize_interface_choice(list(multicast_addresses), ip_version) | ||
| # Strip entries already covered by ``interfaces`` so add_multicast_member | ||
| # is not called twice for the same membership. |
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
Summary
Adds a new
multicast_addressesparameter toZeroconf/AsyncZeroconf/create_socketsso multicast group membership can be configuredindependently of bind interfaces. On iOS (and other sandboxed
environments) the system mDNS daemon owns port 5353 on physical
interfaces so binding there is rejected, but joining the multicast
group on those interfaces is still permitted and necessary to receive
incoming queries.
Today the
interfacesparameter conflates the two roles: everyentry both joins the multicast group and gets a respond socket
bound on it.
multicast_addressesadds an additive escape hatchthat joins additional addresses to the listen socket's multicast
group without creating respond sockets for them. Default behavior is
unchanged.
Fixes #1520
Changes
create_socketsacceptsmulticast_addressesand joins eachnormalized address to the listen socket; rejects the combination
with
unicast=Truesince there is no listen socket.Zeroconf.__init__andAsyncZeroconf.__init__expose theparameter and forward it through.
InterfaceChoice.Defaultsingle-socket fastpath and the per-interface respond-socket path.
InterfaceChoice.Defaultpath, unicastrejection, and
Zeroconf-level wiring.Test plan
poetry run pytest tests/utils/test_net.py tests/test_core.pypoetry run pytest --timeout=60(full suite, 394 passed)poetry run ruff check+ruff format --checkon changedfiles
Quality Report
Changes: 5 files changed, 148 insertions(+), 3 deletions(-)
Code scan: clean
Tests: passed (4 PASSED)
Branch hygiene: clean
Generated by Kōan post-mission quality pipeline