def test_reaper(self): zeroconf = _core.Zeroconf(interfaces=['127.0.0.1']) cache = zeroconf.cache original_entries = list( itertools.chain( *[cache.entries_with_name(name) for name in cache.names()])) record_with_10s_ttl = r.DNSAddress('a', const._TYPE_SOA, const._CLASS_IN, 10, b'a') record_with_1s_ttl = r.DNSAddress('a', const._TYPE_SOA, const._CLASS_IN, 1, b'b') zeroconf.cache.add(record_with_10s_ttl) zeroconf.cache.add(record_with_1s_ttl) entries_with_cache = list( itertools.chain( *[cache.entries_with_name(name) for name in cache.names()])) time.sleep(1) zeroconf.notify_all() time.sleep(0.1) entries = list( itertools.chain( *[cache.entries_with_name(name) for name in cache.names()])) zeroconf.close() assert entries != original_entries assert entries_with_cache != original_entries assert record_with_10s_ttl in entries assert record_with_1s_ttl not in entries
def test_dns_address_record_hashablity(): """Test DNSAddress are hashable.""" address1 = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 1, b'a') address2 = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 1, b'b') address3 = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 1, b'c') address4 = r.DNSAddress('irrelevant', const._TYPE_AAAA, const._CLASS_IN, 1, b'c') record_set = set([address1, address2, address3, address4]) assert len(record_set) == 4 record_set.add(address1) assert len(record_set) == 4 address3_dupe = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 1, b'c') record_set.add(address3_dupe) assert len(record_set) == 4 # Verify we can remove records additional_set = set([address1, address2]) record_set -= additional_set assert record_set == set([address3, address4])
def test_order(self): record1 = r.DNSAddress('a', const._TYPE_SOA, const._CLASS_IN, 1, b'a') record2 = r.DNSAddress('a', const._TYPE_SOA, const._CLASS_IN, 1, b'b') cache = r.DNSCache() cache.add(record1) cache.add(record2) entry = r.DNSEntry('a', const._TYPE_SOA, const._CLASS_IN) cached_record = cache.get(entry) assert cached_record == record2
def test_order(self): record1 = r.DNSAddress("a", r._TYPE_SOA, r._CLASS_IN, 1, b"a") record2 = r.DNSAddress("a", r._TYPE_SOA, r._CLASS_IN, 1, b"b") cache = r.DNSCache() cache.add(record1) cache.add(record2) entry = r.DNSEntry("a", r._TYPE_SOA, r._CLASS_IN) cached_record = cache.get(entry) self.assertEqual(cached_record, record2)
def test_cache_empty_does_not_leak_memory_by_leaving_empty_list(self): record1 = r.DNSAddress('a', const._TYPE_SOA, const._CLASS_IN, 1, b'a') record2 = r.DNSAddress('a', const._TYPE_SOA, const._CLASS_IN, 1, b'b') cache = r.DNSCache() cache.add(record1) cache.add(record2) assert 'a' in cache.cache cache.remove(record1) cache.remove(record2) assert 'a' not in cache.cache
def test_dns_address_repr(self): address = r.DNSAddress('irrelevant', const._TYPE_SOA, const._CLASS_IN, 1, b'a') assert repr(address).endswith("b'a'") address_ipv4 = r.DNSAddress( 'irrelevant', const._TYPE_SOA, const._CLASS_IN, 1, socket.inet_pton(socket.AF_INET, '127.0.0.1') ) assert repr(address_ipv4).endswith('127.0.0.1') address_ipv6 = r.DNSAddress( 'irrelevant', const._TYPE_SOA, const._CLASS_IN, 1, socket.inet_pton(socket.AF_INET6, '::1') ) assert repr(address_ipv6).endswith('::1')
def test_cache_empty_multiple_calls_does_not_throw(self): record1 = r.DNSAddress('a', const._TYPE_SOA, const._CLASS_IN, 1, b'a') record2 = r.DNSAddress('a', const._TYPE_SOA, const._CLASS_IN, 1, b'b') cache = r.DNSCache() cache.add(record1) cache.add(record2) assert 'a' in cache.cache cache.remove(record1) cache.remove(record2) # Ensure multiple removes does not throw cache.remove(record1) cache.remove(record2) assert 'a' not in cache.cache
def test_rrset_does_not_consider_ttl(): """Test DNSRRSet does not consider the ttl in the hash.""" longarec = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 100, b'same') shortarec = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 10, b'same') longaaaarec = r.DNSAddress('irrelevant', const._TYPE_AAAA, const._CLASS_IN, 100, b'same') shortaaaarec = r.DNSAddress('irrelevant', const._TYPE_AAAA, const._CLASS_IN, 10, b'same') rrset = DNSRRSet([longarec, shortaaaarec]) assert rrset.suppresses(longarec) assert rrset.suppresses(shortarec) assert not rrset.suppresses(longaaaarec) assert rrset.suppresses(shortaaaarec) verylongarec = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 1000, b'same') longarec = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 100, b'same') mediumarec = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 60, b'same') shortarec = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, 10, b'same') rrset2 = DNSRRSet([mediumarec]) assert not rrset2.suppresses(verylongarec) assert rrset2.suppresses(longarec) assert rrset2.suppresses(mediumarec) assert rrset2.suppresses(shortarec)
def mock_split_incoming_msg( service_state_change: r.ServiceStateChange) -> r.DNSIncoming: """Mock an incoming message for the case where the packet is split.""" ttl = 120 generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) generated.add_answer_at_time( r.DNSAddress( service_server, const._TYPE_A, const._CLASS_IN | const._CLASS_UNIQUE, ttl, socket.inet_aton(service_address), ), 0, ) generated.add_answer_at_time( r.DNSService( service_name, const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, ttl, 0, 0, 80, service_server, ), 0, ) return r.DNSIncoming(generated.packets()[0])
def test_incoming_unknown_type(self): generated = r.DNSOutgoing(0) answer = r.DNSAddress("a", r._TYPE_SOA, r._CLASS_IN, 1, b"a") generated.add_additional_answer(answer) packet = generated.packet() parsed = r.DNSIncoming(packet) assert len(parsed.answers) == 0 assert parsed.is_query() != parsed.is_response()
def test_dns_record_hashablity_does_not_consider_ttl(): """Test DNSRecord are hashable.""" # Verify the TTL is not considered in the hash record1 = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, const._DNS_OTHER_TTL, b'same') record2 = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, const._DNS_HOST_TTL, b'same') record_set = set([record1, record2]) assert len(record_set) == 1 record_set.add(record1) assert len(record_set) == 1 record3_dupe = r.DNSAddress('irrelevant', const._TYPE_A, const._CLASS_IN, const._DNS_HOST_TTL, b'same') assert record2 == record3_dupe assert record2.__hash__() == record3_dupe.__hash__() record_set.add(record3_dupe) assert len(record_set) == 1
def mock_incoming_msg( service_state_change: r.ServiceStateChange) -> r.DNSIncoming: ttl = 120 generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) if service_state_change == r.ServiceStateChange.Updated: generated.add_answer_at_time( r.DNSText( service_name, const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, ttl, service_text, ), 0, ) return r.DNSIncoming(generated.packets()[0]) if service_state_change == r.ServiceStateChange.Removed: ttl = 0 generated.add_answer_at_time( r.DNSPointer(service_type, const._TYPE_PTR, const._CLASS_IN, ttl, service_name), 0) generated.add_answer_at_time( r.DNSService( service_name, const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, ttl, 0, 0, 80, service_server, ), 0, ) generated.add_answer_at_time( r.DNSText(service_name, const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, ttl, service_text), 0, ) generated.add_answer_at_time( r.DNSAddress( service_server, const._TYPE_A, const._CLASS_IN | const._CLASS_UNIQUE, ttl, socket.inet_aton(service_address), ), 0, ) return r.DNSIncoming(generated.packets()[0])
def test_incoming_ipv6(self): addr = "2606:2800:220:1:248:1893:25c8:1946" # example.com packed = socket.inet_pton(socket.AF_INET6, addr) generated = r.DNSOutgoing(0) answer = r.DNSAddress('domain', r._TYPE_AAAA, r._CLASS_IN, 1, packed) generated.add_additional_answer(answer) packet = generated.packet() parsed = r.DNSIncoming(packet) record = parsed.answers[0] assert isinstance(record, r.DNSAddress) assert record.address == packed
def test_dns_compression_rollback_for_corruption(): """Verify rolling back does not lead to dns compression corruption.""" out = r.DNSOutgoing(const._FLAGS_QR_RESPONSE | const._FLAGS_AA) address = socket.inet_pton(socket.AF_INET, "192.168.208.5") additionals = [ { "name": "HASS Bridge ZJWH FF5137._hap._tcp.local.", "address": address, "port": 51832, "text": b"\x13md=HASS Bridge" b" ZJWH\x06pv=1.0\x14id=01:6B:30:FF:51:37\x05c#=12\x04s#=1\x04ff=0\x04" b"ci=2\x04sf=0\x0bsh=L0m/aQ==", }, { "name": "HASS Bridge 3K9A C2582A._hap._tcp.local.", "address": address, "port": 51834, "text": b"\x13md=HASS Bridge" b" 3K9A\x06pv=1.0\x14id=E2:AA:5B:C2:58:2A\x05c#=12\x04s#=1\x04ff=0\x04" b"ci=2\x04sf=0\x0bsh=b2CnzQ==", }, { "name": "Master Bed TV CEDB27._hap._tcp.local.", "address": address, "port": 51830, "text": b"\x10md=Master Bed" b" TV\x06pv=1.0\x14id=9E:B7:44:CE:DB:27\x05c#=18\x04s#=1\x04ff=0\x05" b"ci=31\x04sf=0\x0bsh=CVj1kw==", }, { "name": "Living Room TV 921B77._hap._tcp.local.", "address": address, "port": 51833, "text": b"\x11md=Living Room" b" TV\x06pv=1.0\x14id=11:61:E7:92:1B:77\x05c#=17\x04s#=1\x04ff=0\x05" b"ci=31\x04sf=0\x0bsh=qU77SQ==", }, { "name": "HASS Bridge ZC8X FF413D._hap._tcp.local.", "address": address, "port": 51829, "text": b"\x13md=HASS Bridge" b" ZC8X\x06pv=1.0\x14id=96:14:45:FF:41:3D\x05c#=12\x04s#=1\x04ff=0\x04" b"ci=2\x04sf=0\x0bsh=b0QZlg==", }, { "name": "HASS Bridge WLTF 4BE61F._hap._tcp.local.", "address": address, "port": 51837, "text": b"\x13md=HASS Bridge" b" WLTF\x06pv=1.0\x14id=E0:E7:98:4B:E6:1F\x04c#=2\x04s#=1\x04ff=0\x04" b"ci=2\x04sf=0\x0bsh=ahAISA==", }, { "name": "FrontdoorCamera 8941D1._hap._tcp.local.", "address": address, "port": 54898, "text": b"\x12md=FrontdoorCamera\x06pv=1.0\x14id=9F:B7:DC:89:41:D1\x04c#=2\x04" b"s#=1\x04ff=0\x04ci=2\x04sf=0\x0bsh=0+MXmA==", }, { "name": "HASS Bridge W9DN 5B5CC5._hap._tcp.local.", "address": address, "port": 51836, "text": b"\x13md=HASS Bridge" b" W9DN\x06pv=1.0\x14id=11:8E:DB:5B:5C:C5\x05c#=12\x04s#=1\x04ff=0\x04" b"ci=2\x04sf=0\x0bsh=6fLM5A==", }, { "name": "HASS Bridge Y9OO EFF0A7._hap._tcp.local.", "address": address, "port": 51838, "text": b"\x13md=HASS Bridge" b" Y9OO\x06pv=1.0\x14id=D3:FE:98:EF:F0:A7\x04c#=2\x04s#=1\x04ff=0\x04" b"ci=2\x04sf=0\x0bsh=u3bdfw==", }, { "name": "Snooze Room TV 6B89B0._hap._tcp.local.", "address": address, "port": 51835, "text": b"\x11md=Snooze Room" b" TV\x06pv=1.0\x14id=5F:D5:70:6B:89:B0\x05c#=17\x04s#=1\x04ff=0\x05" b"ci=31\x04sf=0\x0bsh=xNTqsg==", }, { "name": "AlexanderHomeAssistant 74651D._hap._tcp.local.", "address": address, "port": 54811, "text": b"\x19md=AlexanderHomeAssistant\x06pv=1.0\x14id=59:8A:0B:74:65:1D\x05" b"c#=14\x04s#=1\x04ff=0\x04ci=2\x04sf=0\x0bsh=ccZLPA==", }, { "name": "HASS Bridge OS95 39C053._hap._tcp.local.", "address": address, "port": 51831, "text": b"\x13md=HASS Bridge" b" OS95\x06pv=1.0\x14id=7E:8C:E6:39:C0:53\x05c#=12\x04s#=1\x04ff=0\x04ci=2" b"\x04sf=0\x0bsh=Xfe5LQ==", }, ] out.add_answer_at_time( DNSText( "HASS Bridge W9DN 5B5CC5._hap._tcp.local.", const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, const._DNS_OTHER_TTL, b'\x13md=HASS Bridge W9DN\x06pv=1.0\x14id=11:8E:DB:5B:5C:C5\x05c#=12\x04s#=1' b'\x04ff=0\x04ci=2\x04sf=0\x0bsh=6fLM5A==', ), 0, ) for record in additionals: out.add_additional_answer( r.DNSService( record["name"], # type: ignore const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, const._DNS_HOST_TTL, 0, 0, record["port"], # type: ignore record["name"], # type: ignore )) out.add_additional_answer( r.DNSText( record["name"], # type: ignore const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, const._DNS_OTHER_TTL, record["text"], # type: ignore )) out.add_additional_answer( r.DNSAddress( record["name"], # type: ignore const._TYPE_A, const._CLASS_IN | const._CLASS_UNIQUE, const._DNS_HOST_TTL, record["address"], # type: ignore )) for packet in out.packets(): # Verify we can process the packets we created to # ensure there is no corruption with the dns compression incoming = r.DNSIncoming(packet) assert incoming.valid is True
def test_dns_address_repr(self): address = r.DNSAddress("irrelevant", r._TYPE_SOA, r._CLASS_IN, 1, b"a") repr(address)
def test_dns_address_repr(self): address = r.DNSAddress('irrelevant', r._TYPE_SOA, r._CLASS_IN, 1, b'a') repr(address)
def mock_incoming_msg(service_state_change: r.ServiceStateChange) -> r.DNSIncoming: generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) assert generated.is_response() is True if service_state_change == r.ServiceStateChange.Removed: ttl = 0 else: ttl = 120 generated.add_answer_at_time( r.DNSText( service_name, const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, ttl, service_text ), 0, ) generated.add_answer_at_time( r.DNSService( service_name, const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, ttl, 0, 0, 80, service_server, ), 0, ) # Send the IPv6 address first since we previously # had a bug where the IPv4 would be missing if the # IPv6 was seen first if enable_ipv6: generated.add_answer_at_time( r.DNSAddress( service_server, const._TYPE_AAAA, const._CLASS_IN | const._CLASS_UNIQUE, ttl, socket.inet_pton(socket.AF_INET6, service_v6_address), ), 0, ) generated.add_answer_at_time( r.DNSAddress( service_server, const._TYPE_AAAA, const._CLASS_IN | const._CLASS_UNIQUE, ttl, socket.inet_pton(socket.AF_INET6, service_v6_second_address), ), 0, ) generated.add_answer_at_time( r.DNSAddress( service_server, const._TYPE_A, const._CLASS_IN | const._CLASS_UNIQUE, ttl, socket.inet_aton(service_address), ), 0, ) generated.add_answer_at_time( r.DNSPointer(service_type, const._TYPE_PTR, const._CLASS_IN, ttl, service_name), 0 ) return r.DNSIncoming(generated.packets()[0])
def test_service_info_rejects_non_matching_updates(self): """Verify records with the wrong name are rejected.""" zc = r.Zeroconf(interfaces=['127.0.0.1']) desc = {'path': '/~paulsm/'} service_name = 'name._type._tcp.local.' service_type = '_type._tcp.local.' service_server = 'ash-1.local.' service_address = socket.inet_aton("10.0.1.2") ttl = 120 now = r.current_time_millis() info = ServiceInfo( service_type, service_name, 22, 0, 0, desc, service_server, addresses=[service_address] ) # Verify backwards compatiblity with calling with None info.update_record(zc, now, None) # Matching updates info.update_record( zc, now, r.DNSText( service_name, const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, ttl, b'\x04ff=0\x04ci=2\x04sf=0\x0bsh=6fLM5A==', ), ) assert info.properties[b"ci"] == b"2" info.update_record( zc, now, r.DNSService( service_name, const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, ttl, 0, 0, 80, 'ASH-2.local.', ), ) assert info.server_key == 'ash-2.local.' assert info.server == 'ASH-2.local.' new_address = socket.inet_aton("10.0.1.3") info.update_record( zc, now, r.DNSAddress( 'ASH-2.local.', const._TYPE_A, const._CLASS_IN | const._CLASS_UNIQUE, ttl, new_address, ), ) assert new_address in info.addresses # Non-matching updates info.update_record( zc, now, r.DNSText( "incorrect.name.", const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, ttl, b'\x04ff=0\x04ci=3\x04sf=0\x0bsh=6fLM5A==', ), ) assert info.properties[b"ci"] == b"2" info.update_record( zc, now, r.DNSService( "incorrect.name.", const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, ttl, 0, 0, 80, 'ASH-2.local.', ), ) assert info.server_key == 'ash-2.local.' assert info.server == 'ASH-2.local.' new_address = socket.inet_aton("10.0.1.4") info.update_record( zc, now, r.DNSAddress( "incorrect.name.", const._TYPE_A, const._CLASS_IN | const._CLASS_UNIQUE, ttl, new_address, ), ) assert new_address not in info.addresses zc.close()
def test_get_info_single(self): zc = r.Zeroconf(interfaces=['127.0.0.1']) service_name = 'name._type._tcp.local.' service_type = '_type._tcp.local.' service_server = 'ash-1.local.' service_text = b'path=/~matt1/' service_address = '10.0.1.2' service_info = None send_event = Event() service_info_event = Event() last_sent = None # type: Optional[r.DNSOutgoing] def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT): """Sends an outgoing packet.""" nonlocal last_sent last_sent = out send_event.set() # patch the zeroconf send with unittest.mock.patch.object(zc, "send", send): def mock_incoming_msg(records) -> r.DNSIncoming: generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) for record in records: generated.add_answer_at_time(record, 0) return r.DNSIncoming(generated.packets()[0]) def get_service_info_helper(zc, type, name): nonlocal service_info service_info = zc.get_service_info(type, name) service_info_event.set() try: ttl = 120 helper_thread = threading.Thread( target=get_service_info_helper, args=(zc, service_type, service_name) ) helper_thread.start() wait_time = 1 # Expext query for SRV, TXT, A, AAAA send_event.wait(wait_time) assert last_sent is not None assert len(last_sent.questions) == 4 assert r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN) in last_sent.questions assert r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN) in last_sent.questions assert r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN) in last_sent.questions assert r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN) in last_sent.questions assert service_info is None # Expext no further queries last_sent = None send_event.clear() _inject_response( zc, mock_incoming_msg( [ r.DNSText( service_name, const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, ttl, service_text, ), r.DNSService( service_name, const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, ttl, 0, 0, 80, service_server, ), r.DNSAddress( service_server, const._TYPE_A, const._CLASS_IN | const._CLASS_UNIQUE, ttl, socket.inet_pton(socket.AF_INET, service_address), ), ] ), ) send_event.wait(wait_time) assert last_sent is None assert service_info is not None finally: helper_thread.join() zc.remove_all_service_listeners() zc.close()