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_sending_unicast(): """Test sending unicast response.""" zc = Zeroconf(interfaces=['127.0.0.1']) generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) entry = r.DNSText( "didnotcrashincoming._crash._tcp.local.", const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, 500, b'path=/~paulsm/', ) generated.add_answer_at_time(entry, 0) zc.send(generated, "2001:db8::1", const._MDNS_PORT) # https://www.iana.org/go/rfc3849 time.sleep(0.2) assert zc.cache.get(entry) is None zc.send(generated, "198.51.100.0", const._MDNS_PORT) # Documentation (TEST-NET-2) time.sleep(0.2) assert zc.cache.get(entry) is None zc.send(generated) time.sleep(0.2) assert zc.cache.get(entry) is not None zc.close()
def test_invalid_packets_ignored_and_does_not_cause_loop_exception(): """Ensure an invalid packet cannot cause the loop to collapse.""" zc = Zeroconf(interfaces=['127.0.0.1']) generated = r.DNSOutgoing(0) packet = generated.packets()[0] packet = packet[:8] + b'deadbeef' + packet[8:] parsed = r.DNSIncoming(packet) assert parsed.valid is False mock_out = unittest.mock.Mock() mock_out.packets = lambda: [packet] zc.send(mock_out) generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) entry = r.DNSText( "didnotcrashincoming._crash._tcp.local.", const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, 500, b'path=/~paulsm/', ) assert isinstance(entry, r.DNSText) assert isinstance(entry, r.DNSRecord) assert isinstance(entry, r.DNSEntry) generated.add_answer_at_time(entry, 0) zc.send(generated) time.sleep(0.2) zc.close() assert zc.cache.get(entry) is not None
def test_dns_text_record_hashablity(): """Test DNSText are hashable.""" text1 = r.DNSText('irrelevant', 0, 0, const._DNS_OTHER_TTL, b'12345678901') text2 = r.DNSText('irrelevant', 1, 0, const._DNS_OTHER_TTL, b'12345678901') text3 = r.DNSText('irrelevant', 0, 1, const._DNS_OTHER_TTL, b'12345678901') text4 = r.DNSText('irrelevant', 0, 0, const._DNS_OTHER_TTL, b'ABCDEFGHIJK') record_set = set([text1, text2, text3, text4]) assert len(record_set) == 4 record_set.add(text1) assert len(record_set) == 4 text1_dupe = r.DNSText('irrelevant', 0, 0, const._DNS_OTHER_TTL, b'12345678901') record_set.add(text1_dupe) assert len(record_set) == 4
def test_service_info_rejects_expired_records(self): """Verify records that are expired 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] ) # 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" # Expired record expired_record = r.DNSText( service_name, const._TYPE_TXT, const._CLASS_IN | const._CLASS_UNIQUE, ttl, b'\x04ff=0\x04ci=3\x04sf=0\x0bsh=6fLM5A==', ) expired_record.created = 1000 expired_record._expiration_time = 1000 info.update_record(zc, now, expired_record) assert info.properties[b"ci"] == b"2" zc.close()
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 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