← 输入框|github.com|刷新
Skip to content

fix: don't suppress a scoped copy of a packet as a duplicate of an unscoped one - #1926

Open
akiernan wants to merge 1 commit into
python-zeroconf:masterfrom
akiernan:fix/dedup-per-sender-scope
Open

akiernan wants to merge 1 commit into
python-zeroconf:masterfrom
akiernan:fix/dedup-per-sender-scope

Conversation

@akiernan

@akiernan akiernan commented Oct 23, 2026 •

Copy link
Copy Markdown

Summary

On a dual-stack host, the :: listen socket receives a responder's IPv4 copy of a message, from a v4-mapped sender with scope 0, and its IPv6 copy, from a link-local sender with the interface's scope. The two are byte-identical and usually arrive a few milliseconds apart, IPv4 first. Duplicate suppression compares the payload only, so the IPv6 copy is dropped, although it's the only copy whose link-local AAAA records get a scope. The cache never holds a scoped record, and parsed_scoped_addresses() returns fe80::… with no %<idx> (#1567, the case #1764 left open). The measurements behind this are in #1927.

This PR makes duplicate suppression take into account the scope the packet's records would get. Copies from senders without a scope (IPv4, v4-mapped, global IPv6) still suppress each other as before. A copy from a scoped sender is keyed on its payload and scope together, so it is neither suppressed by an unscoped copy nor suppresses one, and its repeats are still suppressed.

Details

  • RFC 6762 §20: a dual-stack host "acts like a multihomed host, with one connection to the logical 'IPv4 Ethernet segment', and a connection to the logical 'IPv6 Ethernet segment'". An IPv4 copy and an IPv6 copy of a message are separate arrivals, not a duplicate packet.
  • RFC 6762 §6.2: a responder includes its link-local address so "a querier that only has an IPv6 link-local address [can] connect to the link-local address", and SHOULD send its A and AAAA records together in each message. So identical IPv4 and IPv6 copies carrying the AAAA records are normal.
  • RFC 4007 §6: a link-local address is only usable with a zone index, and scope 0 means the default zone, which Linux doesn't have for link-local. That's why the scoped copy is the one that matters.
  • The key is the scope the records would get (addrs[3] for IPv6 senders, 0 otherwise), not the address family. Copies that would scope their records the same way produce identical records, so suppressing them loses nothing, and it avoids a string test for v4-mapped senders on every packet. Scoped copies use a (payload, scope) key in the existing _recent_packets dict, so the bound and the eviction are unchanged.
  • Unchanged: QU handling, the single-slot fast path for repeats from the same kind of sender, and the bound on the recency window.

Test plan

  • New test_guard_against_duplicate_packets_per_sender_scope in tests/test_listener.py. A response carrying a link-local AAAA record is fed from a v4-mapped sender, then from a link-local IPv6 sender: the cache must hold both a scope-0 and a scope-2 record. Repeats of each copy are then still suppressed. It fails on master ({0} == {0, 2}) and passes here.

  • Full suite on Linux (Docker, Python 3.14): 525 passed, in both the pure-Python build (SKIP_CYTHON=1) and the Cython build (REQUIRE_CYTHON=1, listener compiled). test_integration_with_listener_ipv6 fails in some runs in this container on master as well, as "Network is unreachable" for IPv6 multicast.

  • pre-commit run on the changed files: all hooks pass.

  • Microbenchmark of _process_datagram_at_time, Cython build, master against this branch, three interleaved rounds (median ns/packet):

    Packets master this PR
    same payload repeated, IPv4 sender 46.6 46.4
    same payload repeated, IPv6 sender 49.5 48.7
    alternating payloads 50.5 50.3
    unique payloads 698 737 (+6%, within master's 672–747 spread)
  • Real network: a Linux 6.12 host on a link with embedded responders that answer over both IPv4 and IPv6, with an AsyncServiceBrowser for their service type. On master, a 300 s browse logged 43 copies from the responders' fe80:: addresses dropped as duplicates, and no link-local address was ever scoped. On this branch, every responder whose answer arrived over IPv6 multicast had fe80::…%2 in parsed_scoped_addresses() within 10 s. Those whose only answers were IPv4 unicast stayed unscoped (see below).

Not in this PR

A responder that only ever answers by IPv4 unicast (the first, QU, query) still leaves its link-local AAAA records unscoped, since an IPv4 packet has no scope. Scoping those from the receiving interface needs per-interface sockets (#1568) or IP_PKTINFO.

🤖 Generated with Claude Code

…scoped one

A dual-stack listen socket receives a responder's IPv4 copy of a message,
from a v4-mapped sender with scope 0, and its byte-identical IPv6 copy,
from a link-local sender with the interface's scope. Suppressing on the
payload alone dropped the IPv6 copy, the only one whose link-local AAAA
records get a scope, so they were never cached scoped. RFC 6762 section 20
treats a dual-stack host as on two logical segments, so the copies are
separate arrivals.

Key suppression on the scope the records would get as well: unscoped
senders suppress each other as before, and a scoped sender's copy is
keyed on its payload and scope.
@github-actions

Copy link
Copy Markdown

Thanks for contributing! A quick note: this project is in the process of moving from LGPL-2.1 to Apache 2.0, and may keep LGPL-2.1-or-later as an OR option so GPL-2.0-only projects can keep using the library, see #1835.

By continuing with this pull request you agree that this new contribution is offered under both Apache 2.0 and LGPL-2.1-or-later, so it is covered under either outcome.

This notice does not affect your previous contributions. If you would like to consent to relicensing those as well, please post the consent statement on #1835; silence is never treated as consent for past work.

If offering this contribution under these licenses does not work for you, no hard feelings; reply with the exact phrase I do not agree to the Apache 2.0 license or simply close this pull request.

@codecov

codecov Bot commented Oct 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.87%. Comparing base (f0c27b3) to head (729ada9).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1926   +/-   ##
=======================================
  Coverage   99.87%   99.87%           
=======================================
  Files          33       33           
  Lines        3913     3917    +4     
  Branches      568      568           
=======================================
+ Hits         3908     3912    +4     
  Misses          3        3           
  Partials        2        2           

☔ View full report in Codecov by Harness.
📢 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.

This branch has not been deployed

No deployments
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.

1 participant