Skip to content

feat: add multicast_addresses to bind/multicast independently#1767

Draft
bluetoothbot wants to merge 4 commits into
python-zeroconf:masterfrom
bluetoothbot:koan/fix-issue-1520
Draft

feat: add multicast_addresses to bind/multicast independently#1767
bluetoothbot wants to merge 4 commits into
python-zeroconf:masterfrom
bluetoothbot:koan/fix-issue-1520

Conversation

@bluetoothbot
Copy link
Copy Markdown
Contributor

@bluetoothbot bluetoothbot commented May 20, 2026

Summary

Adds a new multicast_addresses parameter to Zeroconf / AsyncZeroconf /
create_sockets so multicast group membership can be configured
independently 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 interfaces parameter conflates the two roles: every
entry both joins the multicast group and gets a respond socket
bound on it. multicast_addresses adds an additive escape hatch
that joins additional addresses to the listen socket's multicast
group without creating respond sockets for them. Default behavior is
unchanged.

Fixes #1520

Changes

  • create_sockets accepts multicast_addresses and joins each
    normalized address to the listen socket; rejects the combination
    with unicast=True since there is no listen socket.
  • Zeroconf.__init__ and AsyncZeroconf.__init__ expose the
    parameter and forward it through.
  • Works on both the InterfaceChoice.Default single-socket fast
    path and the per-interface respond-socket path.
  • Tests cover IPv4, IPv6, InterfaceChoice.Default path, unicast
    rejection, and Zeroconf-level wiring.

Test plan

  • poetry run pytest tests/utils/test_net.py tests/test_core.py
  • poetry run pytest --timeout=60 (full suite, 394 passed)
  • poetry run ruff check + ruff format --check on changed
    files

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

@bdraco bdraco requested a review from Copilot May 20, 2026 20:31
@bdraco
Copy link
Copy Markdown
Member

bdraco commented May 20, 2026

@bluetoothbot rr

@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

❌ Patch coverage is 94.28571% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.71%. Comparing base (e2352ea) to head (e7bc212).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/zeroconf/_utils/net.py 93.54% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1767      +/-   ##
==========================================
- Coverage   99.77%   99.71%   -0.06%     
==========================================
  Files          33       33              
  Lines        3536     3560      +24     
  Branches      498      507       +9     
==========================================
+ Hits         3528     3550      +22     
- Misses          5        6       +1     
- Partials        3        4       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 20, 2026

Merging this PR will not alter performance

✅ 14 untouched benchmarks


Comparing bluetoothbot:koan/fix-issue-1520 (e7bc212) with master (e2352ea)1

Open in CodSpeed

Footnotes

  1. No successful run was found on master (413d3ba) during the generation of this report, so e2352ea was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() with multicast_addresses and reject multicast_addresses with unicast=True.
  • Thread the new parameter through Zeroconf and AsyncZeroconf constructors (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.

Comment thread src/zeroconf/_core.py
Comment on lines +210 to +216
listen_socket, respond_sockets = create_sockets(
interfaces,
unicast,
ip_version,
apple_p2p=apple_p2p,
multicast_addresses=multicast_addresses,
)
Comment on lines 473 to +477
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)
Comment thread tests/test_core.py Outdated
zc = r.Zeroconf(
interfaces=["127.0.0.1"],
multicast_addresses=["192.168.1.5"],
unicast=True,
Comment thread src/zeroconf/_utils/net.py Outdated
Comment on lines +466 to +469
normalized_interfaces = normalize_interface_choice(interfaces, ip_version)
extra_multicast_members = (
normalize_interface_choice(list(multicast_addresses), ip_version) if multicast_addresses else []
)
@bluetoothbot
Copy link
Copy Markdown
Contributor Author

bluetoothbot commented May 20, 2026

PR Review — feat: add multicast_addresses to bind/multicast independently

Implementation is sound and the design is right — separating multicast-group membership from per-interface bind sockets is the correct fix for the iOS sandbox case (issue #1520). Pre-validation rejects IP-version mismatches early, dedup against normalized_interfaces prevents double joins, and both the InterfaceChoice.Default fast path and per-interface path are correctly wired. autodetect_ip_version is now bidirectional w.r.t. multicast_addresses and the four prior @Copilot inline concerns (autodetect including multicast_addresses, IP-version validation, dedup, and the unicast/multicast_addresses test combination) have all been addressed in the current diff. Codecov now reports 99.77% with the changed lines fully covered, which closes @bdraco's coverage CHANGES_REQUESTED. The remaining items are non-blocking: the DRY suggestion that @bdraco flagged (collapse the two extra_multicast_members join loops behind a tiny helper), and a small testing tightening on the IPv6 dedup assertion. No correctness, security, or resource-leak issues found. Once @bdraco's DRY note is either addressed or explicitly waived, this is ready to merge.


🟢 Suggestions

1. DRY the extra_multicast_members join loop (`src/zeroconf/_utils/net.py`, L510-513)

The block for interface in extra_multicast_members: add_multicast_member(listen_socket, interface) appears in both the InterfaceChoice.Default fast path and the per-interface tail. This is what @bdraco's DRY comment is pointing at. A tiny helper like _join_extra_multicast(listen_socket, extra_multicast_members) (or inlining as list(map(...)) won't fly because of the side effect, so a small function is the right shape) collapses the duplication and gives the behaviour one canonical place — useful if you ever want to log per-join outcomes or check return values. Not a blocker, but it removes the only point of accidental drift between the two code paths.

for interface in extra_multicast_members:
    add_multicast_member(cast(socket.socket, listen_socket), interface)

Checklist

  • No hardcoded secrets or credentials
  • Input validation at the API boundary
  • No resource leaks (sockets closed on error)
  • Error handling is precise (no bare except / silent swallow)
  • New code paths are tested
  • Edge cases covered (unicast rejection, IP-version mismatch, dedup, Default path, All)
  • Tests verify observable behaviour, not implementation text
  • Documented in public API docstrings
  • DRY — no duplicated logic that should be a helper — suggestion #1
  • Cython .pxd updates not required (no Cythonized module signatures changed)
  • Conventional-Commits PR title (feat: ...)
  • No Co-Authored-By trailers from automated agents

Summary

Implementation is sound and the design is right — separating multicast-group membership from per-interface bind sockets is the correct fix for the iOS sandbox case (issue #1520). Pre-validation rejects IP-version mismatches early, dedup against normalized_interfaces prevents double joins, and both the InterfaceChoice.Default fast path and per-interface path are correctly wired. autodetect_ip_version is now bidirectional w.r.t. multicast_addresses and the four prior @Copilot inline concerns (autodetect including multicast_addresses, IP-version validation, dedup, and the unicast/multicast_addresses test combination) have all been addressed in the current diff. Codecov now reports 99.77% with the changed lines fully covered, which closes @bdraco's coverage CHANGES_REQUESTED. The remaining items are non-blocking: the DRY suggestion that @bdraco flagged (collapse the two extra_multicast_members join loops behind a tiny helper), and a small testing tightening on the IPv6 dedup assertion. No correctness, security, or resource-leak issues found. Once @bdraco's DRY note is either addressed or explicitly waived, this is ready to merge.


Automated review by Kōanc9cbe65
1a4550e
73e0af3

@bluetoothbot
Copy link
Copy Markdown
Contributor Author

Rebase with requested adjustments

Branch koan/fix-issue-1520 was rebased onto master and review feedback was applied.

Changes applied

  • only interfaces list no longer silently resolves to V4Only (Copilot review on _core.py:216).
  • _utils/net.py: Added explicit IP-version validation for multicast_addresses before any socket work — raises ValueError with a clear message when V4Only sees IPv6 entries (or V6Only sees IPv4) instead of failing later in adapter lookup or setsockopt (Copilot review on _utils/net.py:469).
  • _utils/net.py: Deduplicated extra_multicast_members against normalized_interfaces so addresses appearing in both lists no longer trigger add_multicast_member() twice (Copilot review on _utils/net.py:477).
  • tests/test_core.py: Dropped unicast=True from the forwarding test so it uses a combination the real create_sockets() accepts, keeping the test aligned with the public contract (Copilot review on tests/test_core.py:193).
  • tests/utils/test_net.py: Extended autodetect_ip_version and added focused tests for the new validation paths and the dedup behavior.

Stats

5 files changed, 237 insertions(+), 10 deletions(-)
Actions performed
  • Already-solved check: negative (confidence=high, reasoning=No recent commit on master adds a multicast_addresses parameter to Zeroconf or create_sockets.)
  • Rebased koan/fix-issue-1520 onto upstream/master
  • Applied review feedback
  • Pre-push CI check: previous run passed
  • Force-pushed koan/fix-issue-1520 to origin
  • CI check enqueued in ## CI (async)

CI status

CI will be checked asynchronously.


Automated by Kōan

Copy link
Copy Markdown
Member

@bdraco bdraco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to complete coverage

src/zeroconf/_utils/net.py 96.66% 0 Missing and 1 partia

@bdraco
Copy link
Copy Markdown
Member

bdraco commented May 20, 2026

@bluetoothbot rr

@bluetoothbot
Copy link
Copy Markdown
Contributor Author

Rebase with requested adjustments

Branch koan/fix-issue-1520 was rebased onto master and review feedback was applied.

Changes applied

  • _non_list_interfaces_with_multicast_addressesto cover theisinstance(interfaces, list)False branch inautodetect_ip_versionwhenmulticast_addressesis also supplied (was partial:539->545`).
  • Added test_create_sockets_multicast_addresses_ip_version_all to cover the ip_version == IPVersion.V6Only elif False branch in create_sockets validation, exercised by ip_version=IPVersion.All with multicast_addresses set (was partial: 470->475).
  • Addresses @bdraco's "Need to complete coverage" review (src/zeroconf/_utils/net.py was at 96.66% with 1 partial; now 94% lines with only pre-existing partials remaining — both new partial branches introduced by the PR are now covered).
  • Other inline Copilot comments were already addressed by commit f24fdaf (autodetect now considers multicast_addresses, dedup against interfaces, ip_version validation). The stale Copilot note about unicast=True in the wiring test no longer applies — the current test does not pass unicast=True.

Stats

5 files changed, 273 insertions(+), 10 deletions(-)
Actions performed
  • Already-solved check: negative (confidence=high, reasoning=No recent master commit adds a multicast_addresses parameter to Zeroconf/create_sockets.)
  • Rebased koan/fix-issue-1520 onto upstream/master
  • Applied review feedback
  • Pre-push CI check: previous run passed
  • Force-pushed koan/fix-issue-1520 to origin
  • CI check enqueued in ## CI (async)

CI status

CI will be checked asynchronously.


Automated by Kōan

@bdraco
Copy link
Copy Markdown
Member

bdraco commented May 21, 2026

@bluetoothbot rr

@bluetoothbot
Copy link
Copy Markdown
Contributor Author

Rebase with requested adjustments

Branch koan/fix-issue-1520 was rebased onto master and review feedback was applied.

Stats

5 files changed, 273 insertions(+), 10 deletions(-)
Actions performed
  • Already-solved check: negative (confidence=high, reasoning=No recent master commit adds a multicast_addresses parameter to Zeroconf or create_sockets.)
  • Rebased koan/fix-issue-1520 onto upstream/master
  • Pre-push CI check: previous run passed
  • Force-pushed koan/fix-issue-1520 to origin
  • CI check enqueued in ## CI (async)

CI status

CI will be checked asynchronously.


Automated by Kōan

Copy link
Copy Markdown
Member

@bdraco bdraco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we already have similar logic and this could be made more DRY by breaking out into helpers

@bdraco
Copy link
Copy Markdown
Member

bdraco commented May 21, 2026

@bluetoothbot rr

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
@bluetoothbot
Copy link
Copy Markdown
Contributor Author

Rebase with requested adjustments

Branch koan/fix-issue-1520 was rebased onto master and review feedback was applied.

Changes applied

  • ny(_entry_ip_version(e) == N for e in ...)` guard clauses.
  • Rewrote autodetect_ip_version to build one entries list from interfaces + multicast_addresses and classify via the helper, replacing two parallel any() branches per version that each repeated the isinstance/ipaddress dance.
  • Behavior preserved for all existing tests (61 passed in tests/utils/test_net.py + tests/test_core.py). Tuple entries in interfaces now consistently classify as IPv6 in autodetect — previously only int was checked there, while tuple was already treated as IPv6 in the multicast_addresses path.
  • Coverage on src/zeroconf/_utils/net.py is now 97% with the existing test suite; the codecov bot already reports "All modified and coverable lines are covered by tests" after the prior coverage commit. The Copilot inline comments (autodetect including multicast_addresses, dedup against interfaces, ip_version validation, test using unicast=True) were already addressed in prior commits on this branch.

Stats

5 files changed, 274 insertions(+), 15 deletions(-)
Actions performed
  • Already-solved check: negative (confidence=high, reasoning=No recent master commit adds a multicast_addresses parameter to Zeroconf/create_sockets; recent comm)
  • Rebased koan/fix-issue-1520 onto upstream/master
  • Applied review feedback
  • Pre-push CI check: previous run passed
  • Force-pushed koan/fix-issue-1520 to origin
  • CI check enqueued in ## CI (async)

CI status

CI will be checked asynchronously.


Automated by Kōan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ability to specify multicast groups with addresses

3 participants