| #!/usr/bin/env python3 |
| # |
| # Copyright (c) 2021, The OpenThread Authors. |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # 3. Neither the name of the copyright holder nor the |
| # names of its contributors may be used to endorse or promote products |
| # derived from this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
| # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| # POSSIBILITY OF SUCH DAMAGE. |
| # |
| import ipaddress |
| import typing |
| import unittest |
| |
| import config |
| import thread_cert |
| |
| # Test description: |
| # |
| # This test verifies DNS-SD server and DNS client behavior |
| # (browsing for services and/or subtype services, resolving an |
| # address, or resolving a service). It also indirectly covers the SRP |
| # client and server behavior and the interactions of DNS-SD server |
| # with SRP server). |
| # |
| # Topology: |
| # Four nodes, leader acting as SRP and DNS-SD servers, with 3 router |
| # nodes acting as SRP and DNS clients. |
| # |
| |
| SERVER = 1 |
| CLIENT1 = 2 |
| CLIENT2 = 3 |
| CLIENT3 = 4 |
| |
| DOMAIN = 'default.service.arpa.' |
| SERVICE = '_ipps._tcp' |
| |
| |
| class TestDnssd(thread_cert.TestCase): |
| SUPPORT_NCP = False |
| USE_MESSAGE_FACTORY = False |
| |
| TOPOLOGY = { |
| SERVER: { |
| 'mode': 'rdn', |
| }, |
| CLIENT1: { |
| 'mode': 'rdn', |
| }, |
| CLIENT2: { |
| 'mode': 'rdn', |
| }, |
| CLIENT3: { |
| 'mode': 'rdn', |
| } |
| } |
| |
| def test(self): |
| server = self.nodes[SERVER] |
| client1 = self.nodes[CLIENT1] |
| client2 = self.nodes[CLIENT2] |
| client3 = self.nodes[CLIENT3] |
| |
| #--------------------------------------------------------------- |
| # Start the server & client devices. |
| |
| server.start() |
| self.simulator.go(config.LEADER_STARTUP_DELAY) |
| self.assertEqual(server.get_state(), 'leader') |
| server.srp_server_set_enabled(True) |
| |
| client1.start() |
| client2.start() |
| client3.start() |
| self.simulator.go(config.ROUTER_STARTUP_DELAY) |
| self.assertEqual(client1.get_state(), 'router') |
| self.assertEqual(client2.get_state(), 'router') |
| self.assertEqual(client3.get_state(), 'router') |
| |
| #--------------------------------------------------------------- |
| # Register services on clients |
| |
| client1_addrs = [client1.get_mleid(), client1.get_rloc()] |
| client2_addrs = [client2.get_mleid(), client2.get_rloc()] |
| client3_addrs = [client3.get_mleid(), client2.get_rloc()] |
| |
| self._config_srp_client_services(client1, server, 'ins1', 'host1', 11111, 1, 1, client1_addrs, ",_s1,_s2") |
| self._config_srp_client_services(client2, server, 'ins2', 'HOST2', 22222, 2, 2, client2_addrs) |
| self._config_srp_client_services(client3, server, 'ins3', 'host3', 33333, 3, 3, client3_addrs, ",_S1") |
| |
| #--------------------------------------------------------------- |
| # Resolve address (AAAA records) |
| |
| answers = client1.dns_resolve(f"host1.{DOMAIN}".upper(), server.get_mleid(), 53) |
| self.assertEqual(set(ipaddress.IPv6Address(ip) for ip, _ in answers), |
| set(map(ipaddress.IPv6Address, client1_addrs))) |
| |
| answers = client1.dns_resolve(f"host2.{DOMAIN}", server.get_mleid(), 53) |
| self.assertEqual(set(ipaddress.IPv6Address(ip) for ip, _ in answers), |
| set(map(ipaddress.IPv6Address, client2_addrs))) |
| |
| #--------------------------------------------------------------- |
| # Browsing for services |
| |
| instance1_verify_info = { |
| 'port': 11111, |
| 'priority': 1, |
| 'weight': 1, |
| 'host': 'host1.default.service.arpa.', |
| 'address': client1_addrs, |
| 'txt_data': '', |
| 'srv_ttl': lambda x: x > 0, |
| 'txt_ttl': lambda x: x > 0, |
| 'aaaa_ttl': lambda x: x > 0, |
| } |
| |
| instance2_verify_info = { |
| 'port': 22222, |
| 'priority': 2, |
| 'weight': 2, |
| 'host': 'host2.default.service.arpa.', |
| 'address': client2_addrs, |
| 'txt_data': '', |
| 'srv_ttl': lambda x: x > 0, |
| 'txt_ttl': lambda x: x > 0, |
| 'aaaa_ttl': lambda x: x > 0, |
| } |
| |
| instance3_verify_info = { |
| 'port': 33333, |
| 'priority': 3, |
| 'weight': 3, |
| 'host': 'host3.default.service.arpa.', |
| 'address': client3_addrs, |
| 'txt_data': '', |
| 'srv_ttl': lambda x: x > 0, |
| 'txt_ttl': lambda x: x > 0, |
| 'aaaa_ttl': lambda x: x > 0, |
| } |
| |
| instance4_verify_info = { |
| 'port': 44444, |
| 'priority': 4, |
| 'weight': 4, |
| 'host': 'host3.default.service.arpa.', |
| 'address': client3_addrs, |
| 'txt_data': 'KEY=414243', # KEY=ABC |
| 'srv_ttl': lambda x: x > 0, |
| 'txt_ttl': lambda x: x > 0, |
| 'aaaa_ttl': lambda x: x > 0, |
| } |
| |
| # Browse for main service |
| service_instances = client1.dns_browse(f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) |
| self.assertEqual({'ins1', 'ins2', 'ins3'}, set(service_instances.keys())) |
| |
| # Browse for service sub-type _s1. |
| service_instances = client1.dns_browse(f'_s1._sub.{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) |
| self.assertEqual({'ins1', 'ins3'}, set(service_instances.keys())) |
| |
| # Browse for service sub-type _s2. |
| # Since there is only one matching instance, validate that |
| # server included the service info in additional section. |
| service_instances = client1.dns_browse(f'_s2._sub.{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) |
| self.assertEqual({'ins1'}, set(service_instances.keys())) |
| self._assert_service_instance_equal(service_instances['ins1'], instance1_verify_info) |
| |
| #--------------------------------------------------------------- |
| # Resolve service |
| |
| service_instance = client1.dns_resolve_service('ins1', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) |
| self._assert_service_instance_equal(service_instance, instance1_verify_info) |
| |
| service_instance = client1.dns_resolve_service('ins2', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) |
| self._assert_service_instance_equal(service_instance, instance2_verify_info) |
| |
| service_instance = client1.dns_resolve_service('ins3', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) |
| self._assert_service_instance_equal(service_instance, instance3_verify_info) |
| |
| #--------------------------------------------------------------- |
| # Add another service with TXT entries to the existing host and |
| # verify that it is properly merged. |
| |
| client3.srp_client_add_service('ins4', (SERVICE + ",_s1").upper(), 44444, 4, 4, txt_entries=['KEY=ABC']) |
| self.simulator.go(5) |
| |
| service_instances = client1.dns_browse(f'{SERVICE}.{DOMAIN}', server.get_mleid(), 53) |
| self.assertEqual({'ins1', 'ins2', 'ins3', 'ins4'}, set(service_instances.keys())) |
| |
| service_instance = client1.dns_resolve_service('ins4', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53) |
| self._assert_service_instance_equal(service_instance, instance4_verify_info) |
| |
| def _assert_service_instance_equal(self, instance, info): |
| self.assertEqual(instance['host'].lower(), info['host'].lower(), instance) |
| for f in ('port', 'priority', 'weight', 'txt_data'): |
| self.assertEqual(instance[f], info[f], instance) |
| |
| verify_addresses = info['address'] |
| if not isinstance(verify_addresses, typing.Collection): |
| verify_addresses = [verify_addresses] |
| self.assertIn(ipaddress.IPv6Address(instance['address']), map(ipaddress.IPv6Address, verify_addresses), |
| instance) |
| |
| for ttl_f in ('srv_ttl', 'txt_ttl', 'aaaa_ttl'): |
| check_ttl = info[ttl_f] |
| if not callable(check_ttl): |
| check_ttl = lambda x: x == check_ttl |
| |
| self.assertTrue(check_ttl(instance[ttl_f]), instance) |
| |
| def _config_srp_client_services(self, |
| client, |
| server, |
| instancename, |
| hostname, |
| port, |
| priority, |
| weight, |
| addrs, |
| subtypes=''): |
| client.netdata_show() |
| srp_server_port = client.get_srp_server_port() |
| |
| client.srp_client_start(server.get_mleid(), srp_server_port) |
| client.srp_client_set_host_name(hostname) |
| client.srp_client_set_host_address(*addrs) |
| client.srp_client_add_service(instancename, SERVICE + subtypes, port, priority, weight) |
| |
| self.simulator.go(5) |
| self.assertEqual(client.srp_client_get_host_state(), 'Registered') |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |