def test_parse_own_packet_response(self): generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) generated.add_answer_at_time(r.DNSService( "æøå.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_TTL, 0, 0, 80, "foo.local."), 0) parsed = r.DNSIncoming(generated.packet()) self.assertEqual(len(generated.answers), 1) self.assertEqual(len(generated.answers), len(parsed.answers))
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 generate_host(zc, host_name, type_): name = '.'.join((host_name, type_)) out = r.DNSOutgoing(r._FLAGS_QR_RESPONSE | r._FLAGS_AA) out.add_answer_at_time(r.DNSPointer(type_, r._TYPE_PTR, r._CLASS_IN, r._DNS_OTHER_TTL, name), 0) out.add_answer_at_time( r.DNSService(type_, r._TYPE_SRV, r._CLASS_IN, r._DNS_HOST_TTL, 0, 0, 80, name), 0 ) zc.send(out)
def test_suppress_answer(self): query_generated = r.DNSOutgoing(r._FLAGS_QR_QUERY) question = r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN) query_generated.add_question(question) answer1 = r.DNSService( "testname1.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_HOST_TTL, 0, 0, 80, "foo.local." ) staleanswer2 = r.DNSService( "testname2.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_HOST_TTL / 2, 0, 0, 80, "foo.local." ) answer2 = r.DNSService( "testname2.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_HOST_TTL, 0, 0, 80, "foo.local." ) query_generated.add_answer_at_time(answer1, 0) query_generated.add_answer_at_time(staleanswer2, 0) query = r.DNSIncoming(query_generated.packet()) # Should be suppressed response = r.DNSOutgoing(r._FLAGS_QR_RESPONSE) response.add_answer(query, answer1) assert len(response.answers) == 0 # Should not be suppressed, TTL in query is too short response.add_answer(query, answer2) assert len(response.answers) == 1 # Should not be suppressed, name is different tmp = copy.copy(answer1) tmp.name = "testname3.local." response.add_answer(query, tmp) assert len(response.answers) == 2 # Should not be suppressed, type is different tmp = copy.copy(answer1) tmp.type = r._TYPE_A response.add_answer(query, tmp) assert len(response.answers) == 3 # Should not be suppressed, class is different tmp = copy.copy(answer1) tmp.class_ = r._CLASS_NONE response.add_answer(query, tmp) assert len(response.answers) == 4
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_dns_service_record_hashablity(): """Test DNSService are hashable.""" srv1 = r.DNSService('irrelevant', const._TYPE_SRV, const._CLASS_IN, const._DNS_HOST_TTL, 0, 0, 80, 'a') srv2 = r.DNSService('irrelevant', const._TYPE_SRV, const._CLASS_IN, const._DNS_HOST_TTL, 0, 1, 80, 'a') srv3 = r.DNSService('irrelevant', const._TYPE_SRV, const._CLASS_IN, const._DNS_HOST_TTL, 0, 0, 81, 'a') srv4 = r.DNSService('irrelevant', const._TYPE_SRV, const._CLASS_IN, const._DNS_HOST_TTL, 0, 0, 80, 'ab') record_set = set([srv1, srv2, srv3, srv4]) assert len(record_set) == 4 record_set.add(srv1) assert len(record_set) == 4 srv1_dupe = r.DNSService( 'irrelevant', const._TYPE_SRV, const._CLASS_IN, const._DNS_HOST_TTL, 0, 0, 80, 'a' ) assert srv1 == srv1_dupe assert srv1.__hash__() == srv1_dupe.__hash__() record_set.add(srv1_dupe) assert len(record_set) == 4
def test_only_one_answer_can_by_large(self): """Test that only the first answer in each packet can be large. https://datatracker.ietf.org/doc/html/rfc6762#section-17 """ generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) query = r.DNSIncoming( r.DNSOutgoing(const._FLAGS_QR_QUERY).packets()[0]) for i in range(3): generated.add_answer( query, r.DNSText( "zoom._hap._tcp.local.", const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, 1200, b'\x04ff=0\x04ci=2\x04sf=0\x0bsh=6fLM5A==' * 100, ), ) generated.add_answer( query, r.DNSService( "testname1.local.", const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, const._DNS_HOST_TTL, 0, 0, 80, "foo.local.", ), ) assert len(generated.answers) == 4 packets = generated.packets() assert len(packets) == 4 assert len(packets[0]) <= const._MAX_MSG_ABSOLUTE assert len(packets[0]) > const._MAX_MSG_TYPICAL assert len(packets[1]) <= const._MAX_MSG_ABSOLUTE assert len(packets[1]) > const._MAX_MSG_TYPICAL assert len(packets[2]) <= const._MAX_MSG_ABSOLUTE assert len(packets[2]) > const._MAX_MSG_TYPICAL assert len(packets[3]) <= const._MAX_MSG_TYPICAL for packet in packets: parsed = r.DNSIncoming(packet) assert len(parsed.answers) == 1
def test_adding_expired_answer(self): generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) generated.add_answer_at_time( r.DNSService( "æøå.local.", const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, const._DNS_HOST_TTL, 0, 0, 80, "foo.local.", ), current_time_millis() + 1000000, ) parsed = r.DNSIncoming(generated.packets()[0]) assert len(generated.answers) == 0 assert len(generated.answers) == len(parsed.answers)
def test_parse_own_packet_response(self): generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) generated.add_answer_at_time( r.DNSService( "æøå.local.", const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, const._DNS_HOST_TTL, 0, 0, 80, "foo.local.", ), 0, ) parsed = r.DNSIncoming(generated.packets()[0]) assert len(generated.answers) == 1 assert len(generated.answers) == len(parsed.answers)
def test_questions_do_not_end_up_every_packet(self): """Test that questions are not sent again when multiple packets are needed. https://datatracker.ietf.org/doc/html/rfc6762#section-7.2 Sometimes a Multicast DNS querier will already have too many answers to fit in the Known-Answer Section of its query packets.... It MUST immediately follow the packet with another query packet containing no questions and as many more Known-Answer records as will fit. """ generated = r.DNSOutgoing(const._FLAGS_QR_QUERY) for i in range(35): question = r.DNSQuestion(f"testname{i}.local.", const._TYPE_SRV, const._CLASS_IN) generated.add_question(question) answer = r.DNSService( f"testname{i}.local.", const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, const._DNS_HOST_TTL, 0, 0, 80, f"foo{i}.local.", ) generated.add_answer_at_time(answer, 0) assert len(generated.questions) == 35 assert len(generated.answers) == 35 packets = generated.packets() assert len(packets) == 2 assert len(packets[0]) <= const._MAX_MSG_TYPICAL assert len(packets[1]) <= const._MAX_MSG_TYPICAL parsed1 = r.DNSIncoming(packets[0]) assert len(parsed1.questions) == 35 assert len(parsed1.answers) == 33 parsed2 = r.DNSIncoming(packets[1]) assert len(parsed2.questions) == 0 assert len(parsed2.answers) == 2
def test_dns_service_repr(self): service = r.DNSService("irrelevant", r._TYPE_SRV, r._CLASS_IN, r._DNS_TTL, 0, 0, 80, b"a") repr(service)
def test_dns_service_repr(self): service = r.DNSService('irrelevant', r._TYPE_SRV, r._CLASS_IN, r._DNS_HOST_TTL, 0, 0, 80, b'a') repr(service)
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()
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