1#!/usr/bin/env python3
2#
3#  Copyright (c) 2021, The OpenThread Authors.
4#  All rights reserved.
5#
6#  Redistribution and use in source and binary forms, with or without
7#  modification, are permitted provided that the following conditions are met:
8#  1. Redistributions of source code must retain the above copyright
9#     notice, this list of conditions and the following disclaimer.
10#  2. Redistributions in binary form must reproduce the above copyright
11#     notice, this list of conditions and the following disclaimer in the
12#     documentation and/or other materials provided with the distribution.
13#  3. Neither the name of the copyright holder nor the
14#     names of its contributors may be used to endorse or promote products
15#     derived from this software without specific prior written permission.
16#
17#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27#  POSSIBILITY OF SUCH DAMAGE.
28#
29import ipaddress
30import typing
31import unittest
32
33import config
34import thread_cert
35
36# Test description:
37#
38#   This test verifies DNS-SD server and DNS client behavior
39#   (browsing for services and/or subtype services, resolving an
40#   address, or resolving a service). It also indirectly covers the SRP
41#   client and server behavior and the interactions of DNS-SD server
42#   with SRP server).
43#
44# Topology:
45#    Four nodes, leader acting as SRP and DNS-SD servers, with 3 router
46#    nodes acting as SRP and DNS clients.
47#
48
49SERVER = 1
50CLIENT1 = 2
51CLIENT2 = 3
52CLIENT3 = 4
53
54DOMAIN = 'default.service.arpa.'
55SERVICE = '_ipps._tcp'
56
57
58class TestDnssd(thread_cert.TestCase):
59    SUPPORT_NCP = False
60    USE_MESSAGE_FACTORY = False
61
62    TOPOLOGY = {
63        SERVER: {
64            'mode': 'rdn',
65        },
66        CLIENT1: {
67            'mode': 'rdn',
68        },
69        CLIENT2: {
70            'mode': 'rdn',
71        },
72        CLIENT3: {
73            'mode': 'rdn',
74        }
75    }
76
77    def test(self):
78        server = self.nodes[SERVER]
79        client1 = self.nodes[CLIENT1]
80        client2 = self.nodes[CLIENT2]
81        client3 = self.nodes[CLIENT3]
82
83        #---------------------------------------------------------------
84        # Start the server & client devices.
85
86        server.start()
87        self.simulator.go(config.LEADER_STARTUP_DELAY)
88        self.assertEqual(server.get_state(), 'leader')
89        server.srp_server_set_enabled(True)
90
91        client1.start()
92        client2.start()
93        client3.start()
94        self.simulator.go(config.ROUTER_STARTUP_DELAY)
95        self.assertEqual(client1.get_state(), 'router')
96        self.assertEqual(client2.get_state(), 'router')
97        self.assertEqual(client3.get_state(), 'router')
98
99        #---------------------------------------------------------------
100        # Register services on clients
101
102        client1_addrs = [client1.get_mleid(), client1.get_rloc()]
103        client2_addrs = [client2.get_mleid(), client2.get_rloc()]
104        client3_addrs = [client3.get_mleid(), client2.get_rloc()]
105
106        self._config_srp_client_services(client1, server, 'ins1', 'host1', 11111, 1, 1, client1_addrs, ",_s1,_s2")
107        self._config_srp_client_services(client2, server, 'ins2', 'HOST2', 22222, 2, 2, client2_addrs)
108        self._config_srp_client_services(client3, server, 'ins3', 'host3', 33333, 3, 3, client3_addrs, ",_S1")
109
110        #---------------------------------------------------------------
111        # Resolve address (AAAA records)
112
113        answers = client1.dns_resolve(f"host1.{DOMAIN}".upper(), server.get_mleid(), 53)
114        self.assertEqual(set(ipaddress.IPv6Address(ip) for ip, _ in answers),
115                         set(map(ipaddress.IPv6Address, client1_addrs)))
116
117        answers = client1.dns_resolve(f"host2.{DOMAIN}", server.get_mleid(), 53)
118        self.assertEqual(set(ipaddress.IPv6Address(ip) for ip, _ in answers),
119                         set(map(ipaddress.IPv6Address, client2_addrs)))
120
121        #---------------------------------------------------------------
122        # Browsing for services
123
124        instance1_verify_info = {
125            'port': 11111,
126            'priority': 1,
127            'weight': 1,
128            'host': 'host1.default.service.arpa.',
129            'address': client1_addrs,
130            'txt_data': '',
131            'srv_ttl': lambda x: x > 0,
132            'txt_ttl': lambda x: x > 0,
133            'aaaa_ttl': lambda x: x > 0,
134        }
135
136        instance2_verify_info = {
137            'port': 22222,
138            'priority': 2,
139            'weight': 2,
140            'host': 'host2.default.service.arpa.',
141            'address': client2_addrs,
142            'txt_data': '',
143            'srv_ttl': lambda x: x > 0,
144            'txt_ttl': lambda x: x > 0,
145            'aaaa_ttl': lambda x: x > 0,
146        }
147
148        instance3_verify_info = {
149            'port': 33333,
150            'priority': 3,
151            'weight': 3,
152            'host': 'host3.default.service.arpa.',
153            'address': client3_addrs,
154            'txt_data': '',
155            'srv_ttl': lambda x: x > 0,
156            'txt_ttl': lambda x: x > 0,
157            'aaaa_ttl': lambda x: x > 0,
158        }
159
160        instance4_verify_info = {
161            'port': 44444,
162            'priority': 4,
163            'weight': 4,
164            'host': 'host3.default.service.arpa.',
165            'address': client3_addrs,
166            'txt_data': 'KEY=414243',  # KEY=ABC
167            'srv_ttl': lambda x: x > 0,
168            'txt_ttl': lambda x: x > 0,
169            'aaaa_ttl': lambda x: x > 0,
170        }
171
172        # Browse for main service
173        service_instances = client1.dns_browse(f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53)
174        self.assertEqual({'ins1', 'ins2', 'ins3'}, set(service_instances.keys()))
175
176        # Browse for service sub-type _s1.
177        service_instances = client1.dns_browse(f'_s1._sub.{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53)
178        self.assertEqual({'ins1', 'ins3'}, set(service_instances.keys()))
179
180        # Browse for service sub-type _s2.
181        # Since there is only one matching instance, validate that
182        # server included the service info in additional section.
183        service_instances = client1.dns_browse(f'_s2._sub.{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53)
184        self.assertEqual({'ins1'}, set(service_instances.keys()))
185        self._assert_service_instance_equal(service_instances['ins1'], instance1_verify_info)
186
187        #---------------------------------------------------------------
188        # Resolve service
189
190        service_instance = client1.dns_resolve_service('ins1', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53)
191        self._assert_service_instance_equal(service_instance, instance1_verify_info)
192
193        service_instance = client1.dns_resolve_service('ins2', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53)
194        self._assert_service_instance_equal(service_instance, instance2_verify_info)
195
196        service_instance = client1.dns_resolve_service('ins3', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53)
197        self._assert_service_instance_equal(service_instance, instance3_verify_info)
198
199        #---------------------------------------------------------------
200        # Add another service with TXT entries to the existing host and
201        # verify that it is properly merged.
202
203        client3.srp_client_add_service('ins4', (SERVICE + ",_s1").upper(), 44444, 4, 4, txt_entries=['KEY=ABC'])
204        self.simulator.go(5)
205
206        service_instances = client1.dns_browse(f'{SERVICE}.{DOMAIN}', server.get_mleid(), 53)
207        self.assertEqual({'ins1', 'ins2', 'ins3', 'ins4'}, set(service_instances.keys()))
208
209        service_instance = client1.dns_resolve_service('ins4', f'{SERVICE}.{DOMAIN}'.upper(), server.get_mleid(), 53)
210        self._assert_service_instance_equal(service_instance, instance4_verify_info)
211
212    def _assert_service_instance_equal(self, instance, info):
213        self.assertEqual(instance['host'].lower(), info['host'].lower(), instance)
214        for f in ('port', 'priority', 'weight', 'txt_data'):
215            self.assertEqual(instance[f], info[f], instance)
216
217        verify_addresses = info['address']
218        if not isinstance(verify_addresses, typing.Collection):
219            verify_addresses = [verify_addresses]
220        self.assertIn(ipaddress.IPv6Address(instance['address']), map(ipaddress.IPv6Address, verify_addresses),
221                      instance)
222
223        for ttl_f in ('srv_ttl', 'txt_ttl', 'aaaa_ttl'):
224            check_ttl = info[ttl_f]
225            if not callable(check_ttl):
226                check_ttl = lambda x: x == check_ttl
227
228            self.assertTrue(check_ttl(instance[ttl_f]), instance)
229
230    def _config_srp_client_services(self,
231                                    client,
232                                    server,
233                                    instancename,
234                                    hostname,
235                                    port,
236                                    priority,
237                                    weight,
238                                    addrs,
239                                    subtypes=''):
240        client.netdata_show()
241        srp_server_port = client.get_srp_server_port()
242
243        client.srp_client_start(server.get_mleid(), srp_server_port)
244        client.srp_client_set_host_name(hostname)
245        client.srp_client_set_host_address(*addrs)
246        client.srp_client_add_service(instancename, SERVICE + subtypes, port, priority, weight)
247
248        self.simulator.go(5)
249        self.assertEqual(client.srp_client_get_host_state(), 'Registered')
250
251
252if __name__ == '__main__':
253    unittest.main()
254