Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-zeroconf for openSUSE:Factory 
checked in at 2021-09-07 21:21:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-zeroconf (Old)
 and      /work/SRC/openSUSE:Factory/.python-zeroconf.new.1899 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-zeroconf"

Tue Sep  7 21:21:28 2021 rev:23 rq:917173 version:0.36.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-zeroconf/python-zeroconf.changes  
2021-08-18 08:57:05.810891310 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-zeroconf.new.1899/python-zeroconf.changes    
    2021-09-07 21:22:10.441357543 +0200
@@ -1,0 +2,26 @@
+Mon Sep  6 09:19:13 UTC 2021 - Antonio Larrosa <[email protected]>
+
+- Update to 0.36.2:
+  * Include NSEC records for non-existent types when responding
+    with addresses
+  * Implements RFC6762 sec 6.2
+    (http://datatracker.ietf.org/doc/html/rfc6762#section-6.2)
+
+- Update to 0.36.1:
+  * Skip goodbye packets for addresses when there is another
+    service registered with the same name (#968) @bdraco
+  * If a ServiceInfo that used the same server name as another
+    ServiceInfo was unregistered, goodbye packets would be sent for
+    the addresses and would cause the other service to be seen as
+    offline.
+  * Fixed equality and hash for dns records with the unique bit
+    (#969)
+  * These records should have the same hash and equality since
+    the unique bit (cache flush bit) is not considered when adding
+    or removing the records from the cache.
+
+- Update to 0.36.0:
+  * Technically backwards incompatible:
+  * Fill incomplete IPv6 tuples to avoid WinError on windows (#965)
+
+-------------------------------------------------------------------

Old:
----
  python-zeroconf-0.35.1.tar.xz

New:
----
  python-zeroconf-0.36.2.tar.xz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-zeroconf.spec ++++++
--- /var/tmp/diff_new_pack.IYQz1m/_old  2021-09-07 21:22:10.905358104 +0200
+++ /var/tmp/diff_new_pack.IYQz1m/_new  2021-09-07 21:22:10.909358109 +0200
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define skip_python2 1
 Name:           python-zeroconf
-Version:        0.35.1
+Version:        0.36.2
 Release:        0
 Summary:        Pure Python Multicast DNS Service Discovery Library 
(Bonjour/Avahi compatible)
 License:        LGPL-2.0-only

++++++ _service ++++++
--- /var/tmp/diff_new_pack.IYQz1m/_old  2021-09-07 21:22:10.937358143 +0200
+++ /var/tmp/diff_new_pack.IYQz1m/_new  2021-09-07 21:22:10.941358147 +0200
@@ -2,8 +2,8 @@
   <service name="obs_scm" mode="disabled">
     <param name="url">https://github.com/jstasiak/python-zeroconf</param>
     <param name="scm">git</param>
-    <param name="revision">4281221b668123b770c6d6b0835dd876d1d2f22d</param>
-    <param name="version">0.35.1</param>
+    <param name="revision">0.36.2</param>
+    <param name="version">0.36.2</param>
   </service>
   <service name="set_version" mode="disabled"/>
 

++++++ python-zeroconf-0.35.1.tar.xz -> python-zeroconf-0.36.2.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.35.1/README.rst 
new/python-zeroconf-0.36.2/README.rst
--- old/python-zeroconf-0.35.1/README.rst       2021-08-15 21:14:10.000000000 
+0200
+++ new/python-zeroconf-0.36.2/README.rst       2021-08-30 17:04:19.000000000 
+0200
@@ -75,8 +75,6 @@
 
 * `InterfaceChoice.All` is an alias for `InterfaceChoice.Default` on non-POSIX
   systems.
-* On Windows specific interfaces can only be requested as interface indexes,
-  not as IP addresses.
 * Dual-stack IPv6 sockets are used, which may not be supported everywhere (some
   BSD variants do not have them).
 * Listening on localhost (`::1`) does not work. Help with understanding why is
@@ -140,6 +138,35 @@
 Changelog
 =========
 
+0.36.2
+======
+
+* Include NSEC records for non-existent types when responding with addresses 
(#972) (#971) @bdraco
+  Implements RFC6762 sec 6.2 
(http://datatracker.ietf.org/doc/html/rfc6762#section-6.2)
+
+0.36.1
+======
+
+* Skip goodbye packets for addresses when there is another service registered 
with the same name (#968) @bdraco
+
+  If a ServiceInfo that used the same server name as another ServiceInfo
+  was unregistered, goodbye packets would be sent for the addresses and
+  would cause the other service to be seen as offline.
+* Fixed equality and hash for dns records with the unique bit (#969) @bdraco
+
+  These records should have the same hash and equality since
+  the unique bit (cache flush bit) is not considered when adding or removing
+  the records from the cache.
+
+0.36.0
+======
+
+Technically backwards incompatible:
+
+* Fill incomplete IPv6 tuples to avoid WinError on windows (#965) @lokesh2019
+
+  Fixed #932
+
 0.35.1
 ======
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.35.1/setup.cfg 
new/python-zeroconf-0.36.2/setup.cfg
--- old/python-zeroconf-0.35.1/setup.cfg        2021-08-15 21:14:10.000000000 
+0200
+++ new/python-zeroconf-0.36.2/setup.cfg        2021-08-30 17:04:19.000000000 
+0200
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 0.35.1
+current_version = 0.36.2
 commit = True
 tag = True
 tag_name = {new_version}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-zeroconf-0.35.1/tests/services/test_registry.py 
new/python-zeroconf-0.36.2/tests/services/test_registry.py
--- old/python-zeroconf-0.35.1/tests/services/test_registry.py  2021-08-15 
21:14:10.000000000 +0200
+++ new/python-zeroconf-0.36.2/tests/services/test_registry.py  2021-08-30 
17:04:19.000000000 +0200
@@ -28,6 +28,29 @@
         registry.async_remove(info)
         registry.async_add(info)
 
+    def test_register_same_server(self):
+        type_ = "_test-srvc-type._tcp.local."
+        name = "xxxyyy"
+        name2 = "xxxyyy2"
+        registration_name = "%s.%s" % (name, type_)
+        registration_name2 = "%s.%s" % (name2, type_)
+
+        desc = {'path': '/~paulsm/'}
+        info = ServiceInfo(
+            type_, registration_name, 80, 0, 0, desc, "same.local.", 
addresses=[socket.inet_aton("10.0.1.2")]
+        )
+        info2 = ServiceInfo(
+            type_, registration_name2, 80, 0, 0, desc, "same.local.", 
addresses=[socket.inet_aton("10.0.1.2")]
+        )
+        registry = r.ServiceRegistry()
+        registry.async_add(info)
+        registry.async_add(info2)
+        assert registry.async_get_infos_server("same.local.") == [info, info2]
+        registry.async_remove(info)
+        assert registry.async_get_infos_server("same.local.") == [info2]
+        registry.async_remove(info2)
+        assert registry.async_get_infos_server("same.local.") == []
+
     def test_unregister_multiple_times(self):
         """Verify we can unregister a service multiple times.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.35.1/tests/test_asyncio.py 
new/python-zeroconf-0.36.2/tests/test_asyncio.py
--- old/python-zeroconf-0.35.1/tests/test_asyncio.py    2021-08-15 
21:14:10.000000000 +0200
+++ new/python-zeroconf-0.36.2/tests/test_asyncio.py    2021-08-30 
17:04:19.000000000 +0200
@@ -174,6 +174,140 @@
 
 
 @pytest.mark.asyncio
+async def test_async_service_registration_same_server_different_ports() -> 
None:
+    """Test registering services with the same server with different srv 
records."""
+    aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
+    type_ = "_test1-srvc-type._tcp.local."
+    name = "xxxyyy"
+    name2 = "xxxyyy2"
+
+    registration_name = f"{name}.{type_}"
+    registration_name2 = f"{name2}.{type_}"
+
+    calls = []
+
+    class MyListener(ServiceListener):
+        def add_service(self, zeroconf: Zeroconf, type: str, name: str) -> 
None:
+            calls.append(("add", type, name))
+
+        def remove_service(self, zeroconf: Zeroconf, type: str, name: str) -> 
None:
+            calls.append(("remove", type, name))
+
+        def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> 
None:
+            calls.append(("update", type, name))
+
+    listener = MyListener()
+
+    aiozc.zeroconf.add_service_listener(type_, listener)
+
+    desc = {'path': '/~paulsm/'}
+    info = ServiceInfo(
+        type_,
+        registration_name,
+        80,
+        0,
+        0,
+        desc,
+        "ash-2.local.",
+        addresses=[socket.inet_aton("10.0.1.2")],
+    )
+    info2 = ServiceInfo(
+        type_,
+        registration_name2,
+        81,
+        0,
+        0,
+        desc,
+        "ash-2.local.",
+        addresses=[socket.inet_aton("10.0.1.2")],
+    )
+    tasks = []
+    tasks.append(await aiozc.async_register_service(info))
+    tasks.append(await aiozc.async_register_service(info2))
+    await asyncio.gather(*tasks)
+
+    task = await aiozc.async_unregister_service(info)
+    await task
+    entries = aiozc.zeroconf.cache.async_entries_with_server("ash-2.local.")
+    assert len(entries) == 1
+    assert info2.dns_service() in entries
+    await aiozc.async_close()
+    assert calls == [
+        ('add', type_, registration_name),
+        ('add', type_, registration_name2),
+        ('remove', type_, registration_name),
+        ('remove', type_, registration_name2),
+    ]
+
+
[email protected]
+async def test_async_service_registration_same_server_same_ports() -> None:
+    """Test registering services with the same server with the exact same srv 
record."""
+    aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
+    type_ = "_test1-srvc-type._tcp.local."
+    name = "xxxyyy"
+    name2 = "xxxyyy2"
+
+    registration_name = f"{name}.{type_}"
+    registration_name2 = f"{name2}.{type_}"
+
+    calls = []
+
+    class MyListener(ServiceListener):
+        def add_service(self, zeroconf: Zeroconf, type: str, name: str) -> 
None:
+            calls.append(("add", type, name))
+
+        def remove_service(self, zeroconf: Zeroconf, type: str, name: str) -> 
None:
+            calls.append(("remove", type, name))
+
+        def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> 
None:
+            calls.append(("update", type, name))
+
+    listener = MyListener()
+
+    aiozc.zeroconf.add_service_listener(type_, listener)
+
+    desc = {'path': '/~paulsm/'}
+    info = ServiceInfo(
+        type_,
+        registration_name,
+        80,
+        0,
+        0,
+        desc,
+        "ash-2.local.",
+        addresses=[socket.inet_aton("10.0.1.2")],
+    )
+    info2 = ServiceInfo(
+        type_,
+        registration_name2,
+        80,
+        0,
+        0,
+        desc,
+        "ash-2.local.",
+        addresses=[socket.inet_aton("10.0.1.2")],
+    )
+    tasks = []
+    tasks.append(await aiozc.async_register_service(info))
+    tasks.append(await aiozc.async_register_service(info2))
+    await asyncio.gather(*tasks)
+
+    task = await aiozc.async_unregister_service(info)
+    await task
+    entries = aiozc.zeroconf.cache.async_entries_with_server("ash-2.local.")
+    assert len(entries) == 1
+    assert info2.dns_service() in entries
+    await aiozc.async_close()
+    assert calls == [
+        ('add', type_, registration_name),
+        ('add', type_, registration_name2),
+        ('remove', type_, registration_name),
+        ('remove', type_, registration_name2),
+    ]
+
+
[email protected]
 async def test_async_service_registration_name_conflict() -> None:
     """Test registering services throws on name conflict."""
     aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.35.1/tests/test_dns.py 
new/python-zeroconf-0.36.2/tests/test_dns.py
--- old/python-zeroconf-0.35.1/tests/test_dns.py        2021-08-15 
21:14:10.000000000 +0200
+++ new/python-zeroconf-0.36.2/tests/test_dns.py        2021-08-30 
17:04:19.000000000 +0200
@@ -211,6 +211,21 @@
     assert len(record_set) == 1
 
 
+def test_dns_record_hashablity_does_not_consider_unique():
+    """Test DNSRecord are hashable and unique is ignored."""
+
+    # Verify the unique value is not considered in the hash
+    record1 = r.DNSAddress(
+        'irrelevant', const._TYPE_A, const._CLASS_IN | const._CLASS_UNIQUE, 
const._DNS_OTHER_TTL, b'same'
+    )
+    record2 = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 
const._DNS_OTHER_TTL, b'same')
+
+    assert record1.class_ == record2.class_
+    assert record1.__hash__() == record2.__hash__()
+    record_set = {record1, record2}
+    assert len(record_set) == 1
+
+
 def test_dns_address_record_hashablity():
     """Test DNSAddress are hashable."""
     address1 = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 1, 
b'a')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.35.1/tests/test_handlers.py 
new/python-zeroconf-0.36.2/tests/test_handlers.py
--- old/python-zeroconf-0.35.1/tests/test_handlers.py   2021-08-15 
21:14:10.000000000 +0200
+++ new/python-zeroconf-0.36.2/tests/test_handlers.py   2021-08-30 
17:04:19.000000000 +0200
@@ -108,8 +108,9 @@
         
_process_outgoing_packet(construct_outgoing_multicast_answers(question_answers.mcast_aggregate))
 
         # The additonals should all be suppresed since they are all in the 
answers section
+        # There will be one NSEC additional to indicate the lack of AAAA record
         #
-        assert nbr_answers == 4 and nbr_additionals == 0 and nbr_authorities 
== 0
+        assert nbr_answers == 4 and nbr_additionals == 1 and nbr_authorities 
== 0
         nbr_answers = nbr_additionals = nbr_authorities = 0
 
         # unregister
@@ -143,7 +144,9 @@
             [r.DNSIncoming(packet) for packet in query.packets()], False
         )
         
_process_outgoing_packet(construct_outgoing_multicast_answers(question_answers.mcast_aggregate))
-        assert nbr_answers == 4 and nbr_additionals == 0 and nbr_authorities 
== 0
+
+        # There will be one NSEC additional to indicate the lack of AAAA record
+        assert nbr_answers == 4 and nbr_additionals == 1 and nbr_authorities 
== 0
         nbr_answers = nbr_additionals = nbr_authorities = 0
 
         # unregister
@@ -271,7 +274,9 @@
             has_txt = True
         elif answer.type == const._TYPE_A:
             has_a = True
-    assert nbr_answers == 1 and nbr_additionals == 3
+    assert nbr_answers == 1 and nbr_additionals == 4
+    # There will be one NSEC additional to indicate the lack of AAAA record
+
     assert has_srv and has_txt and has_a
 
     # unregister
@@ -406,7 +411,7 @@
         [r.DNSIncoming(packet) for packet in query.packets()], True
     )
     for answers in (question_answers.ucast, question_answers.mcast_aggregate):
-        has_srv = has_txt = has_a = False
+        has_srv = has_txt = has_a = has_aaaa = has_nsec = False
         nbr_additionals = 0
         nbr_answers = len(answers)
         additionals = set().union(*answers.values())
@@ -418,8 +423,14 @@
                 has_txt = True
             elif answer.type == const._TYPE_A:
                 has_a = True
-        assert nbr_answers == 1 and nbr_additionals == 3
-        assert has_srv and has_txt and has_a
+            elif answer.type == const._TYPE_AAAA:
+                has_aaaa = True
+            elif answer.type == const._TYPE_NSEC:
+                has_nsec = True
+        # There will be one NSEC additional to indicate the lack of AAAA record
+        assert nbr_answers == 1 and nbr_additionals == 4
+        assert has_srv and has_txt and has_a and has_nsec
+        assert not has_aaaa
 
     # unregister
     zc.registry.async_remove(info)
@@ -497,7 +508,7 @@
     zc.register_service(info)
 
     def _validate_complete_response(answers):
-        has_srv = has_txt = has_a = False
+        has_srv = has_txt = has_a = has_aaaa = has_nsec = False
         nbr_answers = len(answers.keys())
         additionals = set().union(*answers.values())
         nbr_additionals = len(additionals)
@@ -509,8 +520,13 @@
                 has_txt = True
             elif answer.type == const._TYPE_A:
                 has_a = True
-        assert nbr_answers == 1 and nbr_additionals == 3
-        assert has_srv and has_txt and has_a
+            elif answer.type == const._TYPE_AAAA:
+                has_aaaa = True
+            elif answer.type == const._TYPE_NSEC:
+                has_nsec = True
+        assert nbr_answers == 1 and nbr_additionals == 4
+        assert has_srv and has_txt and has_a and has_nsec
+        assert not has_aaaa
 
     # With QU should respond to only unicast when the answer has been recently 
multicast
     query = r.DNSOutgoing(const._FLAGS_QR_QUERY)
@@ -635,6 +651,21 @@
     assert not question_answers.mcast_aggregate
     assert not question_answers.mcast_aggregate_last_second
 
+    # Test NSEC record returned when there is no AAAA record and we expectly 
ask
+    generated = r.DNSOutgoing(const._FLAGS_QR_QUERY)
+    question = r.DNSQuestion(server_name, const._TYPE_AAAA, const._CLASS_IN)
+    generated.add_question(question)
+    for dns_address in info.dns_addresses():
+        generated.add_answer_at_time(dns_address, now)
+    packets = generated.packets()
+    question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) 
for packet in packets], False)
+    assert not question_answers.ucast
+    expected_nsec_record: r.DNSNsec = list(question_answers.mcast_now)[0]
+    assert const._TYPE_A not in expected_nsec_record.rdtypes
+    assert const._TYPE_AAAA in expected_nsec_record.rdtypes
+    assert not question_answers.mcast_aggregate
+    assert not question_answers.mcast_aggregate_last_second
+
     # Test SRV supression
     generated = r.DNSOutgoing(const._FLAGS_QR_QUERY)
     question = r.DNSQuestion(registration_name, const._TYPE_SRV, 
const._CLASS_IN)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.35.1/tests/test_protocol.py 
new/python-zeroconf-0.36.2/tests/test_protocol.py
--- old/python-zeroconf-0.35.1/tests/test_protocol.py   2021-08-15 
21:14:10.000000000 +0200
+++ new/python-zeroconf-0.36.2/tests/test_protocol.py   2021-08-30 
17:04:19.000000000 +0200
@@ -54,6 +54,35 @@
         generated.add_question(r.DNSQuestion("testname.local.", 
const._TYPE_SRV, const._CLASS_IN))
         r.DNSIncoming(generated.packets()[0])
 
+    def test_parse_own_packet_nsec(self):
+        answer = r.DNSNsec(
+            'eufy HomeBase2-2464._hap._tcp.local.',
+            const._TYPE_NSEC,
+            const._CLASS_IN | const._CLASS_UNIQUE,
+            const._DNS_OTHER_TTL,
+            'eufy HomeBase2-2464._hap._tcp.local.',
+            [const._TYPE_TXT, const._TYPE_SRV],
+        )
+
+        generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
+        generated.add_answer_at_time(answer, 0)
+        parsed = r.DNSIncoming(generated.packets()[0])
+        assert answer in parsed.answers
+
+        # Types > 255 should be ignored
+        answer_invalid_types = r.DNSNsec(
+            'eufy HomeBase2-2464._hap._tcp.local.',
+            const._TYPE_NSEC,
+            const._CLASS_IN | const._CLASS_UNIQUE,
+            const._DNS_OTHER_TTL,
+            'eufy HomeBase2-2464._hap._tcp.local.',
+            [const._TYPE_TXT, const._TYPE_SRV, 1000],
+        )
+        generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
+        generated.add_answer_at_time(answer_invalid_types, 0)
+        parsed = r.DNSIncoming(generated.packets()[0])
+        assert answer in parsed.answers
+
     def test_parse_own_packet_response(self):
         generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
         generated.add_answer_at_time(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.35.1/zeroconf/__init__.py 
new/python-zeroconf-0.36.2/zeroconf/__init__.py
--- old/python-zeroconf-0.35.1/zeroconf/__init__.py     2021-08-15 
21:14:10.000000000 +0200
+++ new/python-zeroconf-0.36.2/zeroconf/__init__.py     2021-08-30 
17:04:19.000000000 +0200
@@ -80,7 +80,7 @@
 
 __author__ = 'Paul Scott-Murphy, William McBrine'
 __maintainer__ = 'Jakub Stasiak <[email protected]>'
-__version__ = '0.35.1'
+__version__ = '0.36.2'
 __license__ = 'LGPL'
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.35.1/zeroconf/_core.py 
new/python-zeroconf-0.36.2/zeroconf/_core.py
--- old/python-zeroconf-0.35.1/zeroconf/_core.py        2021-08-15 
21:14:10.000000000 +0200
+++ new/python-zeroconf-0.36.2/zeroconf/_core.py        2021-08-30 
17:04:19.000000000 +0200
@@ -571,17 +571,28 @@
         self.registry.async_update(info)
         return asyncio.ensure_future(self._async_broadcast_service(info, 
_REGISTER_TIME, None))
 
-    async def _async_broadcast_service(self, info: ServiceInfo, interval: int, 
ttl: Optional[int]) -> None:
+    async def _async_broadcast_service(
+        self,
+        info: ServiceInfo,
+        interval: int,
+        ttl: Optional[int],
+        broadcast_addresses: bool = True,
+    ) -> None:
         """Send a broadcasts to announce a service at intervals."""
         for i in range(_REGISTER_BROADCASTS):
             if i != 0:
                 await asyncio.sleep(millis_to_seconds(interval))
-            self.async_send(self.generate_service_broadcast(info, ttl))
+            self.async_send(self.generate_service_broadcast(info, ttl, 
broadcast_addresses))
 
-    def generate_service_broadcast(self, info: ServiceInfo, ttl: 
Optional[int]) -> DNSOutgoing:
+    def generate_service_broadcast(
+        self,
+        info: ServiceInfo,
+        ttl: Optional[int],
+        broadcast_addresses: bool = True,
+    ) -> DNSOutgoing:
         """Generate a broadcast to announce a service."""
         out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
-        self._add_broadcast_answer(out, info, ttl)
+        self._add_broadcast_answer(out, info, ttl, broadcast_addresses)
         return out
 
     def generate_service_query(self, info: ServiceInfo) -> DNSOutgoing:  # 
pylint: disable=no-self-use
@@ -600,7 +611,11 @@
         return out
 
     def _add_broadcast_answer(  # pylint: disable=no-self-use
-        self, out: DNSOutgoing, info: ServiceInfo, override_ttl: Optional[int]
+        self,
+        out: DNSOutgoing,
+        info: ServiceInfo,
+        override_ttl: Optional[int],
+        broadcast_addresses: bool = True,
     ) -> None:
         """Add answers to broadcast a service."""
         now = current_time_millis()
@@ -609,8 +624,9 @@
         out.add_answer_at_time(info.dns_pointer(override_ttl=other_ttl, 
created=now), 0)
         out.add_answer_at_time(info.dns_service(override_ttl=host_ttl, 
created=now), 0)
         out.add_answer_at_time(info.dns_text(override_ttl=other_ttl, 
created=now), 0)
-        for dns_address in info.dns_addresses(override_ttl=host_ttl, 
created=now):
-            out.add_answer_at_time(dns_address, 0)
+        if broadcast_addresses:
+            for dns_address in info.dns_addresses(override_ttl=host_ttl, 
created=now):
+                out.add_answer_at_time(dns_address, 0)
 
     def unregister_service(self, info: ServiceInfo) -> None:
         """Unregister a service."""
@@ -622,7 +638,14 @@
     async def async_unregister_service(self, info: ServiceInfo) -> Awaitable:
         """Unregister a service."""
         self.registry.async_remove(info)
-        return asyncio.ensure_future(self._async_broadcast_service(info, 
_UNREGISTER_TIME, 0))
+        # If another server uses the same addresses, we do not want to send
+        # goodbye packets for the address records
+
+        entries = self.registry.async_get_infos_server(info.server)
+        broadcast_addresses = not bool(entries)
+        return asyncio.ensure_future(
+            self._async_broadcast_service(info, _UNREGISTER_TIME, 0, 
broadcast_addresses)
+        )
 
     def generate_unregister_all_services(self) -> Optional[DNSOutgoing]:
         """Generate a DNSOutgoing goodbye for all services and remove them 
from the registry."""
@@ -834,6 +857,11 @@
             out,
             packet,
         )
+        # Get flowinfo and scopeid for the IPV6 socket to create a complete 
IPv6
+        # address tuple: 
https://docs.python.org/3.6/library/socket.html#socket-families
+        if s.family == socket.AF_INET6 and not v6_flow_scope:
+            _, _, sock_flowinfo, sock_scopeid = s.getsockname()
+            v6_flow_scope = (sock_flowinfo, sock_scopeid)
         transport.sendto(packet, (real_addr, port or _MDNS_PORT, 
*v6_flow_scope))
 
     def _close(self) -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.35.1/zeroconf/_dns.py 
new/python-zeroconf-0.36.2/zeroconf/_dns.py
--- old/python-zeroconf-0.35.1/zeroconf/_dns.py 2021-08-15 21:14:10.000000000 
+0200
+++ new/python-zeroconf-0.36.2/zeroconf/_dns.py 2021-08-30 17:04:19.000000000 
+0200
@@ -115,7 +115,7 @@
 
     def __init__(self, name: str, type_: int, class_: int) -> None:
         super().__init__(name, type_, class_)
-        self._hash = hash((self.key, type_, class_))
+        self._hash = hash((self.key, type_, self.class_))
 
     def answered_by(self, rec: 'DNSRecord') -> bool:
         """Returns true if the question is answered by the record"""
@@ -247,7 +247,7 @@
         super().__init__(name, type_, class_, ttl, created)
         self.address = address
         self.scope_id = scope_id
-        self._hash = hash((self.key, type_, class_, address, scope_id))
+        self._hash = hash((self.key, type_, self.class_, address, scope_id))
 
     def write(self, out: 'DNSOutgoing') -> None:
         """Used in constructing an outgoing packet"""
@@ -290,7 +290,7 @@
         super().__init__(name, type_, class_, ttl, created)
         self.cpu = cpu
         self.os = os
-        self._hash = hash((self.key, type_, class_, cpu, os))
+        self._hash = hash((self.key, type_, self.class_, cpu, os))
 
     def write(self, out: 'DNSOutgoing') -> None:
         """Used in constructing an outgoing packet"""
@@ -326,7 +326,7 @@
     ) -> None:
         super().__init__(name, type_, class_, ttl, created)
         self.alias = alias
-        self._hash = hash((self.key, type_, class_, alias))
+        self._hash = hash((self.key, type_, self.class_, alias))
 
     @property
     def max_size_compressed(self) -> int:
@@ -367,7 +367,7 @@
         assert isinstance(text, (bytes, type(None)))
         super().__init__(name, type_, class_, ttl, created)
         self.text = text
-        self._hash = hash((self.key, type_, class_, text))
+        self._hash = hash((self.key, type_, self.class_, text))
 
     def write(self, out: 'DNSOutgoing') -> None:
         """Used in constructing an outgoing packet"""
@@ -411,7 +411,7 @@
         self.weight = weight
         self.port = port
         self.server = server
-        self._hash = hash((self.key, type_, class_, priority, weight, port, 
server))
+        self._hash = hash((self.key, type_, self.class_, priority, weight, 
port, server))
 
     def write(self, out: 'DNSOutgoing') -> None:
         """Used in constructing an outgoing packet"""
@@ -458,8 +458,24 @@
     ) -> None:
         super().__init__(name, type_, class_, ttl, created)
         self.next_name = next_name
-        self.rdtypes = rdtypes
-        self._hash = hash((self.key, type_, class_, next_name, *self.rdtypes))
+        self.rdtypes = sorted(rdtypes)
+        self._hash = hash((self.key, type_, self.class_, next_name, 
*self.rdtypes))
+
+    def write(self, out: 'DNSOutgoing') -> None:
+        """Used in constructing an outgoing packet."""
+        bitmap = bytearray(b'\0' * 32)
+        for rdtype in self.rdtypes:
+            if rdtype > 255:  # mDNS only supports window 0
+                continue
+            offset = rdtype % 256
+            byte = offset // 8
+            total_octets = byte + 1
+            bitmap[byte] |= 0x80 >> (offset % 8)
+        out_bytes = bytes(bitmap[0:total_octets])
+        out.write_name(self.next_name)
+        out.write_short(0)
+        out.write_short(len(out_bytes))
+        out.write_string(out_bytes)
 
     def __eq__(self, other: Any) -> bool:
         """Tests equality on cpu and os"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.35.1/zeroconf/_handlers.py 
new/python-zeroconf-0.36.2/zeroconf/_handlers.py
--- old/python-zeroconf-0.35.1/zeroconf/_handlers.py    2021-08-15 
21:14:10.000000000 +0200
+++ new/python-zeroconf-0.36.2/zeroconf/_handlers.py    2021-08-30 
17:04:19.000000000 +0200
@@ -26,15 +26,17 @@
 from typing import Dict, Iterable, List, NamedTuple, Optional, Set, 
TYPE_CHECKING, Tuple, Union, cast
 
 from ._cache import DNSCache, _UniqueRecordsType
-from ._dns import DNSAddress, DNSPointer, DNSQuestion, DNSRRSet, DNSRecord
+from ._dns import DNSAddress, DNSNsec, DNSPointer, DNSQuestion, DNSRRSet, 
DNSRecord
 from ._history import QuestionHistory
 from ._logger import log
 from ._protocol import DNSIncoming, DNSOutgoing
+from ._services.info import ServiceInfo
 from ._services.registry import ServiceRegistry
 from ._updates import RecordUpdate, RecordUpdateListener
 from ._utils.time import current_time_millis, millis_to_seconds
 from .const import (
     _CLASS_IN,
+    _CLASS_UNIQUE,
     _DNS_OTHER_TTL,
     _DNS_PTR_MIN_TTL,
     _FLAGS_AA,
@@ -44,6 +46,7 @@
     _TYPE_A,
     _TYPE_AAAA,
     _TYPE_ANY,
+    _TYPE_NSEC,
     _TYPE_PTR,
     _TYPE_SRV,
     _TYPE_TXT,
@@ -56,7 +59,8 @@
 _AnswerWithAdditionalsType = Dict[DNSRecord, Set[DNSRecord]]
 
 _MULTICAST_DELAY_RANDOM_INTERVAL = (20, 120)
-_RESPOND_IMMEDIATE_TYPES = {_TYPE_SRV, _TYPE_A, _TYPE_AAAA}
+_ADDRESS_RECORD_TYPES = {_TYPE_A, _TYPE_AAAA}
+_RESPOND_IMMEDIATE_TYPES = {_TYPE_NSEC, _TYPE_SRV, *_ADDRESS_RECORD_TYPES}
 
 
 class QuestionAnswers(NamedTuple):
@@ -78,6 +82,15 @@
     return msg.num_authorities > 0
 
 
+def construct_nsec_record(name: str, types: List[int], now: float) -> DNSNsec:
+    """Construct an NSEC record for name and a list of dns types.
+
+    This function should only be used for SRV/A/AAAA records
+    which have a TTL of _DNS_OTHER_TTL
+    """
+    return DNSNsec(name, _TYPE_NSEC, _CLASS_IN | _CLASS_UNIQUE, 
_DNS_OTHER_TTL, name, types, created=now)
+
+
 def construct_outgoing_multicast_answers(answers: _AnswerWithAdditionalsType) 
-> DNSOutgoing:
     """Add answers and additionals to a DNSOutgoing."""
     out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, multicast=True)
@@ -244,12 +257,23 @@
             # Add recommended additional answers according to
             # https://tools.ietf.org/html/rfc6763#section-12.1.
             dns_pointer = service.dns_pointer(created=now)
-            if not known_answers.suppresses(dns_pointer):
-                answer_set[dns_pointer] = {
-                    service.dns_service(created=now),
-                    service.dns_text(created=now),
-                    *service.dns_addresses(created=now),
-                }
+            if known_answers.suppresses(dns_pointer):
+                continue
+            additionals: Set[DNSRecord] = {service.dns_service(created=now), 
service.dns_text(created=now)}
+            additionals |= self._get_address_and_nsec_records(service, now)
+            answer_set[dns_pointer] = additionals
+
+    def _get_address_and_nsec_records(self, service: ServiceInfo, now: float) 
-> Set[DNSRecord]:
+        """Build a set of address records and NSEC records for non-present 
record types."""
+        seen_types: Set[int] = set()
+        records: Set[DNSRecord] = set()
+        for dns_address in service.dns_addresses(created=now):
+            seen_types.add(dns_address.type)
+            records.add(dns_address)
+        missing_types: Set[int] = _ADDRESS_RECORD_TYPES - seen_types
+        if missing_types:
+            records.add(construct_nsec_record(service.server, 
list(missing_types), now))
+        return records
 
     def _add_address_answers(
         self,
@@ -263,13 +287,21 @@
         for service in self.registry.async_get_infos_server(name):
             answers: List[DNSAddress] = []
             additionals: Set[DNSRecord] = set()
+            seen_types: Set[int] = set()
             for dns_address in service.dns_addresses(created=now):
+                seen_types.add(dns_address.type)
                 if dns_address.type != type_:
                     additionals.add(dns_address)
                 elif not known_answers.suppresses(dns_address):
                     answers.append(dns_address)
-            for answer in answers:
-                answer_set[answer] = additionals
+            missing_types: Set[int] = _ADDRESS_RECORD_TYPES - seen_types
+            if answers:
+                if missing_types:
+                    additionals.add(construct_nsec_record(service.server, 
list(missing_types), now))
+                for answer in answers:
+                    answer_set[answer] = additionals
+            elif type_ in missing_types:
+                answer_set[construct_nsec_record(service.server, 
list(missing_types), now)] = set()
 
     def _answer_question(
         self,
@@ -299,7 +331,7 @@
                     # https://tools.ietf.org/html/rfc6763#section-12.2.
                     dns_service = service.dns_service(created=now)
                     if not known_answers.suppresses(dns_service):
-                        answer_set[dns_service] = 
set(service.dns_addresses(created=now))
+                        answer_set[dns_service] = 
self._get_address_and_nsec_records(service, now)
                 if type_ in (_TYPE_TXT, _TYPE_ANY):
                     dns_text = service.dns_text(created=now)
                     if not known_answers.suppresses(dns_text):

Reply via email to