• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#  Copyright (c) 2016, 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#
29
30import json
31import binascii
32import ipaddress
33import logging
34import os
35import re
36import shlex
37import socket
38import subprocess
39import sys
40import time
41import traceback
42import typing
43import unittest
44from ipaddress import IPv6Address, IPv6Network
45from typing import Union, Dict, Optional, List, Any
46
47import pexpect
48import pexpect.popen_spawn
49
50import config
51import simulator
52import thread_cert
53
54PORT_OFFSET = int(os.getenv('PORT_OFFSET', "0"))
55
56INFRA_DNS64 = int(os.getenv('NAT64', 0))
57
58
59class OtbrDocker:
60    RESET_DELAY = 3
61
62    _socat_proc = None
63    _ot_rcp_proc = None
64    _docker_proc = None
65    _border_routing_counters = None
66
67    def __init__(self, nodeid: int, backbone_network: str, **kwargs):
68        self.verbose = int(float(os.getenv('VERBOSE', 0)))
69
70        assert backbone_network is not None
71        self.backbone_network = backbone_network
72        try:
73            self._docker_name = config.OTBR_DOCKER_NAME_PREFIX + str(nodeid)
74            self._prepare_ot_rcp_sim(nodeid)
75            self._launch_docker()
76        except Exception:
77            traceback.print_exc()
78            self.destroy()
79            raise
80
81    def _prepare_ot_rcp_sim(self, nodeid: int):
82        self._socat_proc = subprocess.Popen(['socat', '-d', '-d', 'pty,raw,echo=0', 'pty,raw,echo=0'],
83                                            stderr=subprocess.PIPE,
84                                            stdin=subprocess.DEVNULL,
85                                            stdout=subprocess.DEVNULL)
86
87        line = self._socat_proc.stderr.readline().decode('ascii').strip()
88        self._rcp_device_pty = rcp_device_pty = line[line.index('PTY is /dev') + 7:]
89        line = self._socat_proc.stderr.readline().decode('ascii').strip()
90        self._rcp_device = rcp_device = line[line.index('PTY is /dev') + 7:]
91        logging.info(f"socat running: device PTY: {rcp_device_pty}, device: {rcp_device}")
92
93        ot_rcp_path = self._get_ot_rcp_path()
94        self._ot_rcp_proc = subprocess.Popen(f"{ot_rcp_path} {nodeid} > {rcp_device_pty} < {rcp_device_pty}",
95                                             shell=True,
96                                             stdin=subprocess.DEVNULL,
97                                             stdout=subprocess.DEVNULL,
98                                             stderr=subprocess.DEVNULL)
99
100        try:
101            self._ot_rcp_proc.wait(1)
102        except subprocess.TimeoutExpired:
103            # We expect ot-rcp not to quit in 1 second.
104            pass
105        else:
106            raise Exception(f"ot-rcp {nodeid} exited unexpectedly!")
107
108    def _get_ot_rcp_path(self) -> str:
109        srcdir = os.environ['top_builddir']
110        path = '%s/examples/apps/ncp/ot-rcp' % srcdir
111        logging.info("ot-rcp path: %s", path)
112        return path
113
114    def _launch_docker(self):
115        logging.info(f'Docker image: {config.OTBR_DOCKER_IMAGE}')
116        subprocess.check_call(f"docker rm -f {self._docker_name} || true", shell=True)
117        CI_ENV = os.getenv('CI_ENV', '').split()
118        dns = ['--dns=127.0.0.1'] if INFRA_DNS64 == 1 else ['--dns=8.8.8.8']
119        nat64_prefix = ['--nat64-prefix', '2001:db8:1:ffff::/96'] if INFRA_DNS64 == 1 else []
120        os.makedirs('/tmp/coverage/', exist_ok=True)
121
122        cmd = ['docker', 'run'] + CI_ENV + [
123            '--rm',
124            '--name',
125            self._docker_name,
126            '--network',
127            self.backbone_network,
128        ] + dns + [
129            '-i',
130            '--sysctl',
131            'net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1',
132            '--privileged',
133            '--cap-add=NET_ADMIN',
134            '--volume',
135            f'{self._rcp_device}:/dev/ttyUSB0',
136            '-v',
137            '/tmp/coverage/:/tmp/coverage/',
138            config.OTBR_DOCKER_IMAGE,
139            '-B',
140            config.BACKBONE_IFNAME,
141            '--trel-url',
142            f'trel://{config.BACKBONE_IFNAME}',
143        ] + nat64_prefix
144        logging.info(' '.join(cmd))
145        self._docker_proc = subprocess.Popen(cmd,
146                                             stdin=subprocess.DEVNULL,
147                                             stdout=sys.stdout if self.verbose else subprocess.DEVNULL,
148                                             stderr=sys.stderr if self.verbose else subprocess.DEVNULL)
149
150        launch_docker_deadline = time.time() + 300
151        launch_ok = False
152
153        while time.time() < launch_docker_deadline:
154            try:
155                subprocess.check_call(f'docker exec -i {self._docker_name} ot-ctl state', shell=True)
156                launch_ok = True
157                logging.info("OTBR Docker %s on %s Is Ready!", self._docker_name, self.backbone_network)
158                break
159            except subprocess.CalledProcessError:
160                time.sleep(5)
161                continue
162
163        assert launch_ok
164
165        self.start_ot_ctl()
166
167    def __repr__(self):
168        return f'OtbrDocker<{self.nodeid}>'
169
170    def start_otbr_service(self):
171        self.bash('service otbr-agent start')
172        self.simulator.go(3)
173        self.start_ot_ctl()
174
175    def stop_otbr_service(self):
176        self.stop_ot_ctl()
177        self.bash('service otbr-agent stop')
178
179    def stop_mdns_service(self):
180        self.bash('service avahi-daemon stop; service mdns stop; !(cat /proc/net/udp | grep -i :14E9)')
181
182    def start_mdns_service(self):
183        self.bash('service avahi-daemon start; service mdns start; cat /proc/net/udp | grep -i :14E9')
184
185    def start_ot_ctl(self):
186        cmd = f'docker exec -i {self._docker_name} ot-ctl'
187        self.pexpect = pexpect.popen_spawn.PopenSpawn(cmd, timeout=30)
188        if self.verbose:
189            self.pexpect.logfile_read = sys.stdout.buffer
190
191        # Add delay to ensure that the process is ready to receive commands.
192        timeout = 0.4
193        while timeout > 0:
194            self.pexpect.send('\r\n')
195            try:
196                self.pexpect.expect('> ', timeout=0.1)
197                break
198            except pexpect.TIMEOUT:
199                timeout -= 0.1
200
201    def stop_ot_ctl(self):
202        self.pexpect.sendeof()
203        self.pexpect.wait()
204        self.pexpect.proc.kill()
205
206    def reserve_udp_port(self, port):
207        self.bash(f'socat -u UDP6-LISTEN:{port},bindtodevice=wpan0 - &')
208
209    def destroy(self):
210        logging.info("Destroying %s", self)
211        self._shutdown_docker()
212        self._shutdown_ot_rcp()
213        self._shutdown_socat()
214
215    def _shutdown_docker(self):
216        if self._docker_proc is None:
217            return
218
219        try:
220            COVERAGE = int(os.getenv('COVERAGE', '0'))
221            OTBR_COVERAGE = int(os.getenv('OTBR_COVERAGE', '0'))
222            test_name = os.getenv('TEST_NAME')
223            unique_node_id = f'{test_name}-{PORT_OFFSET}-{self.nodeid}'
224
225            if COVERAGE or OTBR_COVERAGE:
226                self.bash('service otbr-agent stop')
227
228                cov_file_path = f'/tmp/coverage/coverage-{unique_node_id}.info'
229                # Upload OTBR code coverage if OTBR_COVERAGE=1, otherwise OpenThread code coverage.
230                if OTBR_COVERAGE:
231                    codecov_cmd = f'lcov --directory . --capture --output-file {cov_file_path}'
232                else:
233                    codecov_cmd = ('lcov --directory build/otbr/third_party/openthread/repo --capture '
234                                   f'--output-file {cov_file_path}')
235
236                self.bash(codecov_cmd)
237
238            copyCore = subprocess.run(f'docker cp {self._docker_name}:/core ./coredump_{unique_node_id}', shell=True)
239            if copyCore.returncode == 0:
240                subprocess.check_call(
241                    f'docker cp {self._docker_name}:/usr/sbin/otbr-agent ./otbr-agent_{unique_node_id}', shell=True)
242
243        finally:
244            subprocess.check_call(f"docker rm -f {self._docker_name}", shell=True)
245            self._docker_proc.wait()
246            del self._docker_proc
247
248    def _shutdown_ot_rcp(self):
249        if self._ot_rcp_proc is not None:
250            self._ot_rcp_proc.kill()
251            self._ot_rcp_proc.wait()
252            del self._ot_rcp_proc
253
254    def _shutdown_socat(self):
255        if self._socat_proc is not None:
256            self._socat_proc.stderr.close()
257            self._socat_proc.kill()
258            self._socat_proc.wait()
259            del self._socat_proc
260
261    def bash(self, cmd: str, encoding='ascii') -> List[str]:
262        logging.info("%s $ %s", self, cmd)
263        proc = subprocess.Popen(['docker', 'exec', '-i', self._docker_name, 'bash', '-c', cmd],
264                                stdin=subprocess.DEVNULL,
265                                stdout=subprocess.PIPE,
266                                stderr=sys.stderr,
267                                encoding=encoding)
268
269        with proc:
270
271            lines = []
272
273            while True:
274                line = proc.stdout.readline()
275
276                if not line:
277                    break
278
279                lines.append(line)
280                logging.info("%s $ %r", self, line.rstrip('\r\n'))
281
282            proc.wait()
283
284            if proc.returncode != 0:
285                raise subprocess.CalledProcessError(proc.returncode, cmd, ''.join(lines))
286            else:
287                return lines
288
289    def dns_dig(self, server: str, name: str, qtype: str):
290        """
291        Run dig command to query a DNS server.
292
293        Args:
294            server: the server address.
295            name: the name to query.
296            qtype: the query type (e.g. AAAA, PTR, TXT, SRV).
297
298        Returns:
299            The dig result similar as below:
300            {
301                "opcode": "QUERY",
302                "status": "NOERROR",
303                "id": "64144",
304                "QUESTION": [
305                    ('google.com.', 'IN', 'AAAA')
306                ],
307                "ANSWER": [
308                    ('google.com.', 107,	'IN', 'AAAA', '2404:6800:4008:c00::71'),
309                    ('google.com.', 107,	'IN', 'AAAA', '2404:6800:4008:c00::8a'),
310                    ('google.com.', 107,	'IN', 'AAAA', '2404:6800:4008:c00::66'),
311                    ('google.com.', 107,	'IN', 'AAAA', '2404:6800:4008:c00::8b'),
312                ],
313                "ADDITIONAL": [
314                ],
315            }
316        """
317        output = self.bash(f'dig -6 @{server} \'{name}\' {qtype}', encoding='raw_unicode_escape')
318
319        section = None
320        dig_result = {
321            'QUESTION': [],
322            'ANSWER': [],
323            'ADDITIONAL': [],
324        }
325
326        for line in output:
327            line = line.strip()
328
329            if line.startswith(';; ->>HEADER<<- '):
330                headers = line[len(';; ->>HEADER<<- '):].split(', ')
331                for header in headers:
332                    key, val = header.split(': ')
333                    dig_result[key] = val
334
335                continue
336
337            if line == ';; QUESTION SECTION:':
338                section = 'QUESTION'
339                continue
340            elif line == ';; ANSWER SECTION:':
341                section = 'ANSWER'
342                continue
343            elif line == ';; ADDITIONAL SECTION:':
344                section = 'ADDITIONAL'
345                continue
346            elif section and not line:
347                section = None
348                continue
349
350            if section:
351                assert line
352
353                if section == 'QUESTION':
354                    assert line.startswith(';')
355                    line = line[1:]
356                record = list(line.split())
357
358                if section == 'QUESTION':
359                    if record[2] in ('SRV', 'TXT'):
360                        record[0] = self.__unescape_dns_instance_name(record[0])
361                else:
362                    record[1] = int(record[1])
363                    if record[3] == 'SRV':
364                        record[0] = self.__unescape_dns_instance_name(record[0])
365                        record[4], record[5], record[6] = map(int, [record[4], record[5], record[6]])
366                    elif record[3] == 'TXT':
367                        record[0] = self.__unescape_dns_instance_name(record[0])
368                        record[4:] = [self.__parse_dns_dig_txt(line)]
369                    elif record[3] == 'PTR':
370                        record[4] = self.__unescape_dns_instance_name(record[4])
371
372                dig_result[section].append(tuple(record))
373
374        return dig_result
375
376    def call_dbus_method(self, *args):
377        args = shlex.join([args[0], args[1], json.dumps(args[2:])])
378        return json.loads(
379            self.bash(f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/call_dbus_method.py {args}')
380            [0])
381
382    def get_dbus_property(self, property_name):
383        return self.call_dbus_method('org.freedesktop.DBus.Properties', 'Get', 'io.openthread.BorderRouter',
384                                     property_name)
385
386    def set_dbus_property(self, property_name, property_value):
387        return self.call_dbus_method('org.freedesktop.DBus.Properties', 'Set', 'io.openthread.BorderRouter',
388                                     property_name, property_value)
389
390    def get_border_routing_counters(self):
391        counters = self.get_dbus_property('BorderRoutingCounters')
392        counters = {
393            'inbound_unicast': counters[0],
394            'inbound_multicast': counters[1],
395            'outbound_unicast': counters[2],
396            'outbound_multicast': counters[3],
397            'ra_rx': counters[4],
398            'ra_tx_success': counters[5],
399            'ra_tx_failure': counters[6],
400            'rs_rx': counters[7],
401            'rs_tx_success': counters[8],
402            'rs_tx_failure': counters[9],
403        }
404        logging.info(f'border routing counters: {counters}')
405        return counters
406
407    def _process_traffic_counters(self, counter):
408        return {
409            '4to6': {
410                'packets': counter[0],
411                'bytes': counter[1],
412            },
413            '6to4': {
414                'packets': counter[2],
415                'bytes': counter[3],
416            }
417        }
418
419    def _process_packet_counters(self, counter):
420        return {'4to6': {'packets': counter[0]}, '6to4': {'packets': counter[1]}}
421
422    def nat64_set_enabled(self, enable):
423        return self.call_dbus_method('io.openthread.BorderRouter', 'SetNat64Enabled', enable)
424
425    def activate_ephemeral_key_mode(self, lifetime):
426        return self.call_dbus_method('io.openthread.BorderRouter', 'ActivateEphemeralKeyMode', lifetime)
427
428    def deactivate_ephemeral_key_mode(self, retain_active_session):
429        return self.call_dbus_method('io.openthread.BorderRouter', 'DeactivateEphemeralKeyMode', retain_active_session)
430
431    @property
432    def nat64_cidr(self):
433        self.send_command('nat64 cidr')
434        cidr = self._expect_command_output()[0].strip()
435        return ipaddress.IPv4Network(cidr, strict=False)
436
437    @nat64_cidr.setter
438    def nat64_cidr(self, cidr: ipaddress.IPv4Network):
439        if not isinstance(cidr, ipaddress.IPv4Network):
440            raise ValueError("cidr is expected to be an instance of ipaddress.IPv4Network")
441        self.send_command(f'nat64 cidr {cidr}')
442        self._expect_done()
443
444    @property
445    def nat64_state(self):
446        state = self.get_dbus_property('Nat64State')
447        return {'PrefixManager': state[0], 'Translator': state[1]}
448
449    @property
450    def nat64_mappings(self):
451        return [{
452            'id': row[0],
453            'ip4': row[1],
454            'ip6': row[2],
455            'expiry': row[3],
456            'counters': {
457                'total': self._process_traffic_counters(row[4][0]),
458                'ICMP': self._process_traffic_counters(row[4][1]),
459                'UDP': self._process_traffic_counters(row[4][2]),
460                'TCP': self._process_traffic_counters(row[4][3]),
461            }
462        } for row in self.get_dbus_property('Nat64Mappings')]
463
464    @property
465    def nat64_counters(self):
466        res_error = self.get_dbus_property('Nat64ErrorCounters')
467        res_proto = self.get_dbus_property('Nat64ProtocolCounters')
468        return {
469            'protocol': {
470                'Total': self._process_traffic_counters(res_proto[0]),
471                'ICMP': self._process_traffic_counters(res_proto[1]),
472                'UDP': self._process_traffic_counters(res_proto[2]),
473                'TCP': self._process_traffic_counters(res_proto[3]),
474            },
475            'errors': {
476                'Unknown': self._process_packet_counters(res_error[0]),
477                'Illegal Pkt': self._process_packet_counters(res_error[1]),
478                'Unsup Proto': self._process_packet_counters(res_error[2]),
479                'No Mapping': self._process_packet_counters(res_error[3]),
480            }
481        }
482
483    @property
484    def nat64_traffic_counters(self):
485        res = self.get_dbus_property('Nat64TrafficCounters')
486        return {
487            'Total': self._process_traffic_counters(res[0]),
488            'ICMP': self._process_traffic_counters(res[1]),
489            'UDP': self._process_traffic_counters(res[2]),
490            'TCP': self._process_traffic_counters(res[3]),
491        }
492
493    @property
494    def dns_upstream_query_state(self):
495        return bool(self.get_dbus_property('DnsUpstreamQueryState'))
496
497    @dns_upstream_query_state.setter
498    def dns_upstream_query_state(self, value):
499        if type(value) is not bool:
500            raise ValueError("dns_upstream_query_state must be a bool")
501        return self.set_dbus_property('DnsUpstreamQueryState', value)
502
503    @property
504    def ephemeral_key_enabled(self):
505        return bool(self.get_dbus_property('EphemeralKeyEnabled'))
506
507    @ephemeral_key_enabled.setter
508    def ephemeral_key_enabled(self, value):
509        if type(value) is not bool:
510            raise ValueError("ephemeral_key_enabled must be a bool")
511        return self.set_dbus_property('EphemeralKeyEnabled', value)
512
513    def read_border_routing_counters_delta(self):
514        old_counters = self._border_routing_counters
515        new_counters = self.get_border_routing_counters()
516        self._border_routing_counters = new_counters
517        delta_counters = {}
518        if old_counters is None:
519            delta_counters = new_counters
520        else:
521            for i in ('inbound', 'outbound'):
522                for j in ('unicast', 'multicast'):
523                    key = f'{i}_{j}'
524                    assert (key in old_counters)
525                    assert (key in new_counters)
526                    value = [new_counters[key][0] - old_counters[key][0], new_counters[key][1] - old_counters[key][1]]
527                    delta_counters[key] = value
528        delta_counters = {
529            key: value for key, value in delta_counters.items() if not isinstance(value, int) and value[0] and value[1]
530        }
531
532        return delta_counters
533
534    @staticmethod
535    def __unescape_dns_instance_name(name: str) -> str:
536        new_name = []
537        i = 0
538        while i < len(name):
539            c = name[i]
540
541            if c == '\\':
542                assert i + 1 < len(name), name
543                if name[i + 1].isdigit():
544                    assert i + 3 < len(name) and name[i + 2].isdigit() and name[i + 3].isdigit(), name
545                    new_name.append(chr(int(name[i + 1:i + 4])))
546                    i += 3
547                else:
548                    new_name.append(name[i + 1])
549                    i += 1
550            else:
551                new_name.append(c)
552
553            i += 1
554
555        return ''.join(new_name)
556
557    def __parse_dns_dig_txt(self, line: str):
558        # Example TXT entry:
559        # "xp=\\000\\013\\184\\000\\000\\000\\000\\000"
560        txt = {}
561        for entry in re.findall(r'"((?:[^\\]|\\.)*?)"', line):
562            if entry == "":
563                continue
564
565            k, v = entry.split('=', 1)
566            txt[k] = v
567
568        return txt
569
570    def _setup_sysctl(self):
571        self.bash(f'sysctl net.ipv6.conf.{self.ETH_DEV}.accept_ra=2')
572        self.bash(f'sysctl net.ipv6.conf.{self.ETH_DEV}.accept_ra_rt_info_max_plen=64')
573
574
575class OtCli:
576    RESET_DELAY = 0.1
577
578    def __init__(self, nodeid, is_mtd=False, version=None, is_bbr=False, **kwargs):
579        self.verbose = int(float(os.getenv('VERBOSE', 0)))
580        self.node_type = os.getenv('NODE_TYPE', 'sim')
581        self.env_version = os.getenv('THREAD_VERSION', '1.1')
582        self.is_bbr = is_bbr
583        self._initialized = False
584        if os.getenv('COVERAGE', 0) and os.getenv('CC', 'gcc') == 'gcc':
585            self._cmd_prefix = '/usr/bin/env GCOV_PREFIX=%s/ot-run/%s/ot-gcda.%d ' % (os.getenv(
586                'top_srcdir', '.'), sys.argv[0], nodeid)
587        else:
588            self._cmd_prefix = ''
589
590        if version is not None:
591            self.version = version
592        else:
593            self.version = self.env_version
594
595        mode = os.environ.get('USE_MTD') == '1' and is_mtd and 'mtd' or 'ftd'
596
597        if self.node_type == 'soc':
598            self.__init_soc(nodeid)
599        elif self.node_type == 'ncp-sim':
600            # TODO use mode after ncp-mtd is available.
601            self.__init_ncp_sim(nodeid, 'ftd')
602        else:
603            self.__init_sim(nodeid, mode)
604
605        if self.verbose:
606            self.pexpect.logfile_read = sys.stdout.buffer
607
608        self._initialized = True
609
610    def __init_sim(self, nodeid, mode):
611        """ Initialize a simulation node. """
612
613        # Default command if no match below, will be overridden if below conditions are met.
614        cmd = './ot-cli-%s' % (mode)
615
616        # For Thread 1.2 MTD node, use ot-cli-mtd build regardless of OT_CLI_PATH
617        if self.version != '1.1' and mode == 'mtd' and 'top_builddir' in os.environ:
618            srcdir = os.environ['top_builddir']
619            cmd = '%s/examples/apps/cli/ot-cli-%s %d' % (srcdir, mode, nodeid)
620
621        # If Thread version of node matches the testing environment version.
622        elif self.version == self.env_version:
623            # Load Thread 1.2 BBR device when testing Thread 1.2 scenarios
624            # which requires device with Backbone functionality.
625            if self.version != '1.1' and self.is_bbr:
626                if 'OT_CLI_PATH_BBR' in os.environ:
627                    cmd = os.environ['OT_CLI_PATH_BBR']
628                elif 'top_builddir_1_4_bbr' in os.environ:
629                    srcdir = os.environ['top_builddir_1_4_bbr']
630                    cmd = '%s/examples/apps/cli/ot-cli-%s' % (srcdir, mode)
631
632            # Load Thread device of the testing environment version (may be 1.1 or 1.2)
633            else:
634                if 'OT_CLI_PATH' in os.environ:
635                    cmd = os.environ['OT_CLI_PATH']
636                elif 'top_builddir' in os.environ:
637                    srcdir = os.environ['top_builddir']
638                    cmd = '%s/examples/apps/cli/ot-cli-%s' % (srcdir, mode)
639
640            if 'RADIO_DEVICE' in os.environ:
641                cmd += ' --real-time-signal=+1 -v spinel+hdlc+uart://%s?forkpty-arg=%d' % (os.environ['RADIO_DEVICE'],
642                                                                                           nodeid)
643                self.is_posix = True
644            else:
645                cmd += ' %d' % nodeid
646
647        # Load Thread 1.1 node when testing Thread 1.2 scenarios for interoperability
648        elif self.version == '1.1':
649            # Posix app
650            if 'OT_CLI_PATH_1_1' in os.environ:
651                cmd = os.environ['OT_CLI_PATH_1_1']
652            elif 'top_builddir_1_1' in os.environ:
653                srcdir = os.environ['top_builddir_1_1']
654                cmd = '%s/examples/apps/cli/ot-cli-%s' % (srcdir, mode)
655
656            if 'RADIO_DEVICE_1_1' in os.environ:
657                cmd += ' --real-time-signal=+1 -v spinel+hdlc+uart://%s?forkpty-arg=%d' % (
658                    os.environ['RADIO_DEVICE_1_1'], nodeid)
659                self.is_posix = True
660            else:
661                cmd += ' %d' % nodeid
662
663        print("%s" % cmd)
664
665        self.pexpect = pexpect.popen_spawn.PopenSpawn(self._cmd_prefix + cmd, timeout=10)
666
667        # Add delay to ensure that the process is ready to receive commands.
668        timeout = 0.4
669        while timeout > 0:
670            self.pexpect.send('\r\n')
671            try:
672                self.pexpect.expect('> ', timeout=0.1)
673                break
674            except pexpect.TIMEOUT:
675                timeout -= 0.1
676
677    def __init_ncp_sim(self, nodeid, mode):
678        """ Initialize an NCP simulation node. """
679
680        # Default command if no match below, will be overridden if below conditions are met.
681        cmd = 'spinel-cli.py -p ./ot-ncp-%s -n' % mode
682
683        # If Thread version of node matches the testing environment version.
684        if self.version == self.env_version:
685            if 'RADIO_DEVICE' in os.environ:
686                args = ' --real-time-signal=+1 spinel+hdlc+uart://%s?forkpty-arg=%d' % (os.environ['RADIO_DEVICE'],
687                                                                                        nodeid)
688                self.is_posix = True
689            else:
690                args = ''
691
692            # Load Thread 1.2 BBR device when testing Thread 1.2 scenarios
693            # which requires device with Backbone functionality.
694            if self.version != '1.1' and self.is_bbr:
695                if 'OT_NCP_PATH_1_4_BBR' in os.environ:
696                    cmd = 'spinel-cli.py -p "%s%s" -n' % (
697                        os.environ['OT_NCP_PATH_1_4_BBR'],
698                        args,
699                    )
700                elif 'top_builddir_1_4_bbr' in os.environ:
701                    srcdir = os.environ['top_builddir_1_4_bbr']
702                    cmd = '%s/examples/apps/ncp/ot-ncp-%s' % (srcdir, mode)
703                    cmd = 'spinel-cli.py -p "%s%s" -n' % (
704                        cmd,
705                        args,
706                    )
707
708            # Load Thread device of the testing environment version (may be 1.1 or 1.2).
709            else:
710                if 'OT_NCP_PATH' in os.environ:
711                    cmd = 'spinel-cli.py -p "%s%s" -n' % (
712                        os.environ['OT_NCP_PATH'],
713                        args,
714                    )
715                elif 'top_builddir' in os.environ:
716                    srcdir = os.environ['top_builddir']
717                    cmd = '%s/examples/apps/ncp/ot-ncp-%s' % (srcdir, mode)
718                    cmd = 'spinel-cli.py -p "%s%s" -n' % (
719                        cmd,
720                        args,
721                    )
722
723        # Load Thread 1.1 node when testing Thread 1.2 scenarios for interoperability.
724        elif self.version == '1.1':
725            if 'RADIO_DEVICE_1_1' in os.environ:
726                args = ' --real-time-signal=+1 spinel+hdlc+uart://%s?forkpty-arg=%d' % (os.environ['RADIO_DEVICE_1_1'],
727                                                                                        nodeid)
728                self.is_posix = True
729            else:
730                args = ''
731
732            if 'OT_NCP_PATH_1_1' in os.environ:
733                cmd = 'spinel-cli.py -p "%s%s" -n' % (
734                    os.environ['OT_NCP_PATH_1_1'],
735                    args,
736                )
737            elif 'top_builddir_1_1' in os.environ:
738                srcdir = os.environ['top_builddir_1_1']
739                cmd = '%s/examples/apps/ncp/ot-ncp-%s' % (srcdir, mode)
740                cmd = 'spinel-cli.py -p "%s%s" -n' % (
741                    cmd,
742                    args,
743                )
744
745        cmd += ' %d' % nodeid
746        print("%s" % cmd)
747
748        self.pexpect = pexpect.spawn(self._cmd_prefix + cmd, timeout=10)
749
750        # Add delay to ensure that the process is ready to receive commands.
751        time.sleep(0.2)
752        self._expect('spinel-cli >')
753        self.debug(int(os.getenv('DEBUG', '0')))
754
755    def __init_soc(self, nodeid):
756        """ Initialize a System-on-a-chip node connected via UART. """
757        import fdpexpect
758
759        serialPort = '/dev/ttyUSB%d' % ((nodeid - 1) * 2)
760        self.pexpect = fdpexpect.fdspawn(os.open(serialPort, os.O_RDWR | os.O_NONBLOCK | os.O_NOCTTY))
761
762    def destroy(self):
763        if not self._initialized:
764            return
765
766        if (hasattr(self.pexpect, 'proc') and self.pexpect.proc.poll() is None or
767                not hasattr(self.pexpect, 'proc') and self.pexpect.isalive()):
768            print("%d: exit" % self.nodeid)
769            self.pexpect.send('exit\n')
770            self.pexpect.expect(pexpect.EOF)
771            self.pexpect.wait()
772            self._initialized = False
773
774
775class NodeImpl:
776    is_host = False
777    is_otbr = False
778
779    def __init__(self, nodeid, name=None, simulator=None, **kwargs):
780        self.nodeid = nodeid
781        self.name = name or ('Node%d' % nodeid)
782        self.is_posix = False
783
784        self.simulator = simulator
785        if self.simulator:
786            self.simulator.add_node(self)
787
788        super().__init__(nodeid, **kwargs)
789
790        self.set_addr64('%016x' % (thread_cert.EXTENDED_ADDRESS_BASE + nodeid))
791
792    def _expect(self, pattern, timeout=-1, *args, **kwargs):
793        """ Process simulator events until expected the pattern. """
794        if timeout == -1:
795            timeout = self.pexpect.timeout
796
797        assert timeout > 0
798
799        while timeout > 0:
800            try:
801                return self.pexpect.expect(pattern, 0.1, *args, **kwargs)
802            except pexpect.TIMEOUT:
803                timeout -= 0.1
804                self.simulator.go(0)
805                if timeout <= 0:
806                    raise
807
808    def _expect_done(self, timeout=-1):
809        self._expect('Done', timeout)
810
811    def _expect_result(self, pattern, *args, **kwargs):
812        """Expect a single matching result.
813
814        The arguments are identical to pexpect.expect().
815
816        Returns:
817            The matched line.
818        """
819        results = self._expect_results(pattern, *args, **kwargs)
820        assert len(results) == 1, results
821        return results[0]
822
823    def _expect_results(self, pattern, *args, **kwargs):
824        """Expect multiple matching results.
825
826        The arguments are identical to pexpect.expect().
827
828        Returns:
829            The matched lines.
830        """
831        output = self._expect_command_output()
832        results = [line for line in output if self._match_pattern(line, pattern)]
833        return results
834
835    def _expect_key_value_pairs(self, pattern, separator=': '):
836        """Expect 'key: value' in multiple lines.
837
838        Returns:
839            Dictionary of the key:value pairs.
840        """
841        result = {}
842        for line in self._expect_results(pattern):
843            key, val = line.split(separator)
844            result.update({key: val})
845        return result
846
847    @staticmethod
848    def _match_pattern(line, pattern):
849        if isinstance(pattern, str):
850            pattern = re.compile(pattern)
851
852        if isinstance(pattern, typing.Pattern):
853            return pattern.match(line)
854        else:
855            return any(NodeImpl._match_pattern(line, p) for p in pattern)
856
857    def _expect_command_output(self, ignore_logs=True):
858        lines = []
859
860        while True:
861            line = self.__readline(ignore_logs=ignore_logs)
862
863            if line == 'Done':
864                break
865            elif line.startswith('Error '):
866                raise Exception(line)
867            else:
868                lines.append(line)
869
870        print(f'_expect_command_output() returns {lines!r}')
871        return lines
872
873    def __is_logging_line(self, line: str) -> bool:
874        return len(line) >= 3 and line[:3] in {'[D]', '[I]', '[N]', '[W]', '[C]', '[-]'}
875
876    def read_cert_messages_in_commissioning_log(self, timeout=-1):
877        """Get the log of the traffic after DTLS handshake.
878        """
879        format_str = br"=+?\[\[THCI\].*?type=%s.*?\].*?=+?[\s\S]+?-{40,}"
880        join_fin_req = format_str % br"JOIN_FIN\.req"
881        join_fin_rsp = format_str % br"JOIN_FIN\.rsp"
882        dummy_format_str = br"\[THCI\].*?type=%s.*?"
883        join_ent_ntf = dummy_format_str % br"JOIN_ENT\.ntf"
884        join_ent_rsp = dummy_format_str % br"JOIN_ENT\.rsp"
885        pattern = (b"(" + join_fin_req + b")|(" + join_fin_rsp + b")|(" + join_ent_ntf + b")|(" + join_ent_rsp + b")")
886
887        messages = []
888        # There are at most 4 cert messages both for joiner and commissioner
889        for _ in range(0, 4):
890            try:
891                self._expect(pattern, timeout=timeout)
892                log = self.pexpect.match.group(0)
893                messages.append(self._extract_cert_message(log))
894            except BaseException:
895                break
896        return messages
897
898    def _extract_cert_message(self, log):
899        res = re.search(br"direction=\w+", log)
900        assert res
901        direction = res.group(0).split(b'=')[1].strip()
902
903        res = re.search(br"type=\S+", log)
904        assert res
905        type = res.group(0).split(b'=')[1].strip()
906
907        payload = bytearray([])
908        payload_len = 0
909        if type in [b"JOIN_FIN.req", b"JOIN_FIN.rsp"]:
910            res = re.search(br"len=\d+", log)
911            assert res
912            payload_len = int(res.group(0).split(b'=')[1].strip())
913
914            hex_pattern = br"\|(\s([0-9a-fA-F]{2}|\.\.))+?\s+?\|"
915            while True:
916                res = re.search(hex_pattern, log)
917                if not res:
918                    break
919                data = [int(hex, 16) for hex in res.group(0)[1:-1].split(b' ') if hex and hex != b'..']
920                payload += bytearray(data)
921                log = log[res.end() - 1:]
922        assert len(payload) == payload_len
923        return (direction, type, payload)
924
925    def send_command(self, cmd, go=True, expect_command_echo=True):
926        print("%d: %s" % (self.nodeid, cmd))
927        self.pexpect.send(cmd + '\n')
928        if go:
929            self.simulator.go(0, nodeid=self.nodeid)
930        sys.stdout.flush()
931
932        if expect_command_echo:
933            self._expect_command_echo(cmd)
934
935    def _expect_command_echo(self, cmd):
936        cmd = cmd.strip()
937        while True:
938            line = self.__readline()
939            if line.strip() == cmd:
940                break
941
942            logging.warning("expecting echo %r, but read %r", cmd, line)
943
944    def __readline(self, ignore_logs=True):
945        PROMPT = 'spinel-cli > ' if self.node_type == 'ncp-sim' else '> '
946        while True:
947            self._expect(r"[^\n]+\n")
948            line = self.pexpect.match.group(0).decode('utf8').strip()
949            while line.startswith(PROMPT):
950                line = line[len(PROMPT):]
951
952            if line == '':
953                continue
954
955            if ignore_logs and self.__is_logging_line(line):
956                continue
957
958            return line
959
960    def get_commands(self):
961        self.send_command('?')
962        self._expect('Commands:')
963        return self._expect_results(r'\S+')
964
965    def set_mode(self, mode):
966        cmd = 'mode %s' % mode
967        self.send_command(cmd)
968        self._expect_done()
969
970    def debug(self, level):
971        # `debug` command will not trigger interaction with simulator
972        self.send_command('debug %d' % level, go=False)
973
974    def start(self):
975        self.interface_up()
976        self.thread_start()
977
978    def stop(self):
979        self.thread_stop()
980        self.interface_down()
981
982    def set_log_level(self, level: int):
983        self.send_command(f'log level {level}')
984        self._expect_done()
985
986    def interface_up(self):
987        self.send_command('ifconfig up')
988        self._expect_done()
989
990    def interface_down(self):
991        self.send_command('ifconfig down')
992        self._expect_done()
993
994    def thread_start(self):
995        self.send_command('thread start')
996        self._expect_done()
997
998    def thread_stop(self):
999        self.send_command('thread stop')
1000        self._expect_done()
1001
1002    def detach(self, is_async=False):
1003        cmd = 'detach'
1004        if is_async:
1005            cmd += ' async'
1006
1007        self.send_command(cmd)
1008
1009        if is_async:
1010            self._expect_done()
1011            return
1012
1013        end = self.simulator.now() + 4
1014        while True:
1015            self.simulator.go(1)
1016            try:
1017                self._expect_done(timeout=0.1)
1018                return
1019            except (pexpect.TIMEOUT, socket.timeout):
1020                if self.simulator.now() > end:
1021                    raise
1022
1023    def expect_finished_detaching(self):
1024        self._expect('Finished detaching')
1025
1026    def commissioner_start(self):
1027        cmd = 'commissioner start'
1028        self.send_command(cmd)
1029        self._expect_done()
1030
1031    def commissioner_stop(self):
1032        cmd = 'commissioner stop'
1033        self.send_command(cmd)
1034        self._expect_done()
1035
1036    def commissioner_state(self):
1037        states = [r'disabled', r'petitioning', r'active']
1038        self.send_command('commissioner state')
1039        return self._expect_result(states)
1040
1041    def commissioner_add_joiner(self, addr, psk):
1042        cmd = 'commissioner joiner add %s %s' % (addr, psk)
1043        self.send_command(cmd)
1044        self._expect_done()
1045
1046    def commissioner_set_provisioning_url(self, provisioning_url=''):
1047        cmd = 'commissioner provisioningurl %s' % provisioning_url
1048        self.send_command(cmd)
1049        self._expect_done()
1050
1051    def joiner_start(self, pskd='', provisioning_url=''):
1052        cmd = 'joiner start %s %s' % (pskd, provisioning_url)
1053        self.send_command(cmd)
1054        self._expect_done()
1055
1056    def clear_allowlist(self):
1057        cmd = 'macfilter addr clear'
1058        self.send_command(cmd)
1059        self._expect_done()
1060
1061    def enable_allowlist(self):
1062        cmd = 'macfilter addr allowlist'
1063        self.send_command(cmd)
1064        self._expect_done()
1065
1066    def disable_allowlist(self):
1067        cmd = 'macfilter addr disable'
1068        self.send_command(cmd)
1069        self._expect_done()
1070
1071    def add_allowlist(self, addr, rssi=None):
1072        cmd = 'macfilter addr add %s' % addr
1073
1074        if rssi is not None:
1075            cmd += ' %s' % rssi
1076
1077        self.send_command(cmd)
1078        self._expect_done()
1079
1080    def radiofilter_is_enabled(self) -> bool:
1081        states = [r'Disabled', r'Enabled']
1082        self.send_command('radiofilter')
1083        return self._expect_result(states) == 'Enabled'
1084
1085    def radiofilter_enable(self):
1086        cmd = 'radiofilter enable'
1087        self.send_command(cmd)
1088        self._expect_done()
1089
1090    def radiofilter_disable(self):
1091        cmd = 'radiofilter disable'
1092        self.send_command(cmd)
1093        self._expect_done()
1094
1095    def get_bbr_registration_jitter(self):
1096        self.send_command('bbr jitter')
1097        return int(self._expect_result(r'\d+'))
1098
1099    def set_bbr_registration_jitter(self, jitter):
1100        cmd = 'bbr jitter %d' % jitter
1101        self.send_command(cmd)
1102        self._expect_done()
1103
1104    def get_rcp_version(self) -> str:
1105        self.send_command('rcp version')
1106        rcp_version = self._expect_command_output()[0].strip()
1107        return rcp_version
1108
1109    def srp_server_get_state(self):
1110        states = ['disabled', 'running', 'stopped']
1111        self.send_command('srp server state')
1112        return self._expect_result(states)
1113
1114    def srp_server_get_addr_mode(self):
1115        modes = [r'unicast', r'anycast']
1116        self.send_command(f'srp server addrmode')
1117        return self._expect_result(modes)
1118
1119    def srp_server_set_addr_mode(self, mode):
1120        self.send_command(f'srp server addrmode {mode}')
1121        self._expect_done()
1122
1123    def srp_server_get_anycast_seq_num(self):
1124        self.send_command(f'srp server seqnum')
1125        return int(self._expect_result(r'\d+'))
1126
1127    def srp_server_set_anycast_seq_num(self, seqnum):
1128        self.send_command(f'srp server seqnum {seqnum}')
1129        self._expect_done()
1130
1131    def srp_server_set_enabled(self, enable):
1132        cmd = f'srp server {"enable" if enable else "disable"}'
1133        self.send_command(cmd)
1134        self._expect_done()
1135
1136    def srp_server_set_lease_range(self, min_lease, max_lease, min_key_lease, max_key_lease):
1137        self.send_command(f'srp server lease {min_lease} {max_lease} {min_key_lease} {max_key_lease}')
1138        self._expect_done()
1139
1140    def srp_server_set_ttl_range(self, min_ttl, max_ttl):
1141        self.send_command(f'srp server ttl {min_ttl} {max_ttl}')
1142        self._expect_done()
1143
1144    def srp_server_get_hosts(self):
1145        """Returns the host list on the SRP server as a list of property
1146           dictionary.
1147
1148           Example output:
1149           [{
1150               'fullname': 'my-host.default.service.arpa.',
1151               'name': 'my-host',
1152               'deleted': 'false',
1153               'addresses': ['2001::1', '2001::2']
1154           }]
1155        """
1156
1157        cmd = 'srp server host'
1158        self.send_command(cmd)
1159        lines = self._expect_command_output()
1160        host_list = []
1161        while lines:
1162            host = {}
1163
1164            host['fullname'] = lines.pop(0).strip()
1165            host['name'] = host['fullname'].split('.')[0]
1166
1167            host['deleted'] = lines.pop(0).strip().split(':')[1].strip()
1168            if host['deleted'] == 'true':
1169                host_list.append(host)
1170                continue
1171
1172            addresses = lines.pop(0).strip().split('[')[1].strip(' ]').split(',')
1173            map(str.strip, addresses)
1174            host['addresses'] = [addr.strip() for addr in addresses if addr]
1175
1176            host_list.append(host)
1177
1178        return host_list
1179
1180    def srp_server_get_host(self, host_name):
1181        """Returns host on the SRP server that matches given host name.
1182
1183           Example usage:
1184           self.srp_server_get_host("my-host")
1185        """
1186
1187        for host in self.srp_server_get_hosts():
1188            if host_name == host['name']:
1189                return host
1190
1191    def srp_server_get_services(self):
1192        """Returns the service list on the SRP server as a list of property
1193           dictionary.
1194
1195           Example output:
1196           [{
1197               'fullname': 'my-service._ipps._tcp.default.service.arpa.',
1198               'instance': 'my-service',
1199               'name': '_ipps._tcp',
1200               'deleted': 'false',
1201               'port': '12345',
1202               'priority': '0',
1203               'weight': '0',
1204               'ttl': '7200',
1205               'lease': '7200',
1206               'key-lease': '7200',
1207               'TXT': ['abc=010203'],
1208               'host_fullname': 'my-host.default.service.arpa.',
1209               'host': 'my-host',
1210               'addresses': ['2001::1', '2001::2']
1211           }]
1212
1213           Note that the TXT data is output as a HEX string.
1214        """
1215
1216        cmd = 'srp server service'
1217        self.send_command(cmd)
1218        lines = self._expect_command_output()
1219
1220        service_list = []
1221        while lines:
1222            service = {}
1223
1224            service['fullname'] = lines.pop(0).strip()
1225            name_labels = service['fullname'].split('.')
1226            service['instance'] = name_labels[0]
1227            service['name'] = '.'.join(name_labels[1:3])
1228
1229            service['deleted'] = lines.pop(0).strip().split(':')[1].strip()
1230            if service['deleted'] == 'true':
1231                service_list.append(service)
1232                continue
1233
1234            # 'subtypes', port', 'priority', 'weight', 'ttl', 'lease', and 'key-lease'
1235            for i in range(0, 7):
1236                key_value = lines.pop(0).strip().split(':')
1237                service[key_value[0].strip()] = key_value[1].strip()
1238
1239            txt_entries = lines.pop(0).strip().split('[')[1].strip(' ]').split(',')
1240            txt_entries = map(str.strip, txt_entries)
1241            service['TXT'] = [txt for txt in txt_entries if txt]
1242
1243            service['host_fullname'] = lines.pop(0).strip().split(':')[1].strip()
1244            service['host'] = service['host_fullname'].split('.')[0]
1245
1246            addresses = lines.pop(0).strip().split('[')[1].strip(' ]').split(',')
1247            addresses = map(str.strip, addresses)
1248            service['addresses'] = [addr for addr in addresses if addr]
1249
1250            service_list.append(service)
1251
1252        return service_list
1253
1254    def srp_server_get_service(self, instance_name, service_name):
1255        """Returns service on the SRP server that matches given instance
1256           name and service name.
1257
1258           Example usage:
1259           self.srp_server_get_service("my-service", "_ipps._tcp")
1260        """
1261
1262        for service in self.srp_server_get_services():
1263            if (instance_name == service['instance'] and service_name == service['name']):
1264                return service
1265
1266    def get_srp_server_port(self):
1267        """Returns the SRP server UDP port by parsing
1268           the SRP Server Data in Network Data.
1269        """
1270
1271        for service in self.get_services():
1272            # TODO: for now, we are using 0xfd as the SRP service data.
1273            #       May use a dedicated bit flag for SRP server.
1274            if int(service[1], 16) == 0x5d:
1275                # The SRP server data contains IPv6 address (16 bytes)
1276                # followed by UDP port number.
1277                return int(service[2][2 * 16:], 16)
1278
1279    def srp_client_start(self, server_address, server_port):
1280        self.send_command(f'srp client start {server_address} {server_port}')
1281        self._expect_done()
1282
1283    def srp_client_stop(self):
1284        self.send_command(f'srp client stop')
1285        self._expect_done()
1286
1287    def srp_client_get_state(self):
1288        cmd = 'srp client state'
1289        self.send_command(cmd)
1290        return self._expect_command_output()[0]
1291
1292    def srp_client_get_auto_start_mode(self):
1293        cmd = 'srp client autostart'
1294        self.send_command(cmd)
1295        return self._expect_command_output()[0]
1296
1297    def srp_client_enable_auto_start_mode(self):
1298        self.send_command(f'srp client autostart enable')
1299        self._expect_done()
1300
1301    def srp_client_disable_auto_start_mode(self):
1302        self.send_command(f'srp client autostart disable')
1303        self._expect_done()
1304
1305    def srp_client_get_server_address(self):
1306        cmd = 'srp client server address'
1307        self.send_command(cmd)
1308        return self._expect_command_output()[0]
1309
1310    def srp_client_get_server_port(self):
1311        cmd = 'srp client server port'
1312        self.send_command(cmd)
1313        return int(self._expect_command_output()[0])
1314
1315    def srp_client_get_host_state(self):
1316        cmd = 'srp client host state'
1317        self.send_command(cmd)
1318        return self._expect_command_output()[0]
1319
1320    def srp_client_set_host_name(self, name):
1321        self.send_command(f'srp client host name {name}')
1322        self._expect_done()
1323
1324    def srp_client_get_host_name(self):
1325        self.send_command(f'srp client host name')
1326        self._expect_done()
1327
1328    def srp_client_remove_host(self, remove_key=False, send_unreg_to_server=False):
1329        self.send_command(f'srp client host remove {int(remove_key)} {int(send_unreg_to_server)}')
1330        self._expect_done()
1331
1332    def srp_client_clear_host(self):
1333        self.send_command(f'srp client host clear')
1334        self._expect_done()
1335
1336    def srp_client_enable_auto_host_address(self):
1337        self.send_command(f'srp client host address auto')
1338        self._expect_done()
1339
1340    def srp_client_set_host_address(self, *addrs: str):
1341        self.send_command(f'srp client host address {" ".join(addrs)}')
1342        self._expect_done()
1343
1344    def srp_client_get_host_address(self):
1345        self.send_command(f'srp client host address')
1346        self._expect_done()
1347
1348    def srp_client_add_service(self,
1349                               instance_name,
1350                               service_name,
1351                               port,
1352                               priority=0,
1353                               weight=0,
1354                               txt_entries=[],
1355                               lease=0,
1356                               key_lease=0):
1357        txt_record = "".join(self._encode_txt_entry(entry) for entry in txt_entries)
1358        if txt_record == '':
1359            txt_record = '-'
1360        instance_name = self._escape_escapable(instance_name)
1361        self.send_command(
1362            f'srp client service add {instance_name} {service_name} {port} {priority} {weight} {txt_record} {lease} {key_lease}'
1363        )
1364        self._expect_done()
1365
1366    def srp_client_remove_service(self, instance_name, service_name):
1367        self.send_command(f'srp client service remove {instance_name} {service_name}')
1368        self._expect_done()
1369
1370    def srp_client_clear_service(self, instance_name, service_name):
1371        self.send_command(f'srp client service clear {instance_name} {service_name}')
1372        self._expect_done()
1373
1374    def srp_client_get_services(self):
1375        cmd = 'srp client service'
1376        self.send_command(cmd)
1377        service_lines = self._expect_command_output()
1378        return [self._parse_srp_client_service(line) for line in service_lines]
1379
1380    def srp_client_set_lease_interval(self, leaseinterval: int):
1381        cmd = f'srp client leaseinterval {leaseinterval}'
1382        self.send_command(cmd)
1383        self._expect_done()
1384
1385    def srp_client_get_lease_interval(self) -> int:
1386        cmd = 'srp client leaseinterval'
1387        self.send_command(cmd)
1388        return int(self._expect_result('\d+'))
1389
1390    def srp_client_set_key_lease_interval(self, leaseinterval: int):
1391        cmd = f'srp client keyleaseinterval {leaseinterval}'
1392        self.send_command(cmd)
1393        self._expect_done()
1394
1395    def srp_client_get_key_lease_interval(self) -> int:
1396        cmd = 'srp client keyleaseinterval'
1397        self.send_command(cmd)
1398        return int(self._expect_result('\d+'))
1399
1400    def srp_client_set_ttl(self, ttl: int):
1401        cmd = f'srp client ttl {ttl}'
1402        self.send_command(cmd)
1403        self._expect_done()
1404
1405    def srp_client_get_ttl(self) -> int:
1406        cmd = 'srp client ttl'
1407        self.send_command(cmd)
1408        return int(self._expect_result('\d+'))
1409
1410    #
1411    # TREL utilities
1412    #
1413
1414    def enable_trel(self):
1415        cmd = 'trel enable'
1416        self.send_command(cmd)
1417        self._expect_done()
1418
1419    def is_trel_enabled(self) -> Union[None, bool]:
1420        states = [r'Disabled', r'Enabled']
1421        self.send_command('trel')
1422        try:
1423            return self._expect_result(states) == 'Enabled'
1424        except Exception as ex:
1425            if 'InvalidCommand' in str(ex):
1426                return None
1427
1428            raise
1429
1430    def get_trel_counters(self):
1431        cmd = 'trel counters'
1432        self.send_command(cmd)
1433        result = self._expect_command_output()
1434
1435        counters = {}
1436        for line in result:
1437            m = re.match(r'(\w+)\:[^\d]+(\d+)[^\d]+(\d+)(?:[^\d]+(\d+))?', line)
1438            if m:
1439                groups = m.groups()
1440                sub_counters = {
1441                    'packets': int(groups[1]),
1442                    'bytes': int(groups[2]),
1443                }
1444                if groups[3]:
1445                    sub_counters['failures'] = int(groups[3])
1446                counters[groups[0]] = sub_counters
1447        return counters
1448
1449    def reset_trel_counters(self):
1450        cmd = 'trel counters reset'
1451        self.send_command(cmd)
1452        self._expect_done()
1453
1454    def get_trel_port(self):
1455        cmd = 'trel port'
1456        self.send_command(cmd)
1457        return int(self._expect_command_output()[0])
1458
1459    def get_border_agent_counters(self):
1460        cmd = 'ba counters'
1461        self.send_command(cmd)
1462        result = self._expect_command_output()
1463
1464        counters = {}
1465        for line in result:
1466            m = re.match(r'(\w+)\: (\d+)', line)
1467            if m:
1468                counter_name = m.group(1)
1469                counter_value = m.group(2)
1470
1471                counters[counter_name] = int(counter_value)
1472        return counters
1473
1474    def _encode_txt_entry(self, entry):
1475        """Encodes the TXT entry to the DNS-SD TXT record format as a HEX string.
1476
1477           Example usage:
1478           self._encode_txt_entries(['abc'])     -> '03616263'
1479           self._encode_txt_entries(['def='])    -> '046465663d'
1480           self._encode_txt_entries(['xyz=XYZ']) -> '0778797a3d58595a'
1481        """
1482        return '{:02x}'.format(len(entry)) + "".join("{:02x}".format(ord(c)) for c in entry)
1483
1484    def _parse_srp_client_service(self, line: str):
1485        """Parse one line of srp service list into a dictionary which
1486           maps string keys to string values.
1487
1488           Example output for input
1489           'instance:\"%s\", name:\"%s\", state:%s, port:%d, priority:%d, weight:%d"'
1490           {
1491               'instance': 'my-service',
1492               'name': '_ipps._udp',
1493               'state': 'ToAdd',
1494               'port': '12345',
1495               'priority': '0',
1496               'weight': '0'
1497           }
1498
1499           Note that value of 'port', 'priority' and 'weight' are represented
1500           as strings but not integers.
1501        """
1502        key_values = [word.strip().split(':') for word in line.split(', ')]
1503        keys = [key_value[0] for key_value in key_values]
1504        values = [key_value[1].strip('"') for key_value in key_values]
1505        return dict(zip(keys, values))
1506
1507    def locate(self, anycast_addr):
1508        cmd = 'locate ' + anycast_addr
1509        self.send_command(cmd)
1510        self.simulator.go(5)
1511        return self._parse_locate_result(self._expect_command_output()[0])
1512
1513    def _parse_locate_result(self, line: str):
1514        """Parse anycast locate result as list of ml-eid and rloc16.
1515
1516           Example output for input
1517           'fd00:db8:0:0:acf9:9d0:7f3c:b06e 0xa800'
1518
1519           [ 'fd00:db8:0:0:acf9:9d0:7f3c:b06e', '0xa800' ]
1520        """
1521        return line.split(' ')
1522
1523    def enable_backbone_router(self):
1524        cmd = 'bbr enable'
1525        self.send_command(cmd)
1526        self._expect_done()
1527
1528    def disable_backbone_router(self):
1529        cmd = 'bbr disable'
1530        self.send_command(cmd)
1531        self._expect_done()
1532
1533    def register_backbone_router(self):
1534        cmd = 'bbr register'
1535        self.send_command(cmd)
1536        self._expect_done()
1537
1538    def get_backbone_router_state(self):
1539        states = [r'Disabled', r'Primary', r'Secondary']
1540        self.send_command('bbr state')
1541        return self._expect_result(states)
1542
1543    @property
1544    def is_primary_backbone_router(self) -> bool:
1545        return self.get_backbone_router_state() == 'Primary'
1546
1547    def get_backbone_router(self):
1548        cmd = 'bbr config'
1549        self.send_command(cmd)
1550        self._expect(r'(.*)Done')
1551        g = self.pexpect.match.groups()
1552        output = g[0].decode("utf-8")
1553        lines = output.strip().split('\n')
1554        lines = [l.strip() for l in lines]
1555        ret = {}
1556        for l in lines:
1557            z = re.search(r'seqno:\s+([0-9]+)', l)
1558            if z:
1559                ret['seqno'] = int(z.groups()[0])
1560
1561            z = re.search(r'delay:\s+([0-9]+)', l)
1562            if z:
1563                ret['delay'] = int(z.groups()[0])
1564
1565            z = re.search(r'timeout:\s+([0-9]+)', l)
1566            if z:
1567                ret['timeout'] = int(z.groups()[0])
1568
1569        return ret
1570
1571    def set_backbone_router(self, seqno=None, reg_delay=None, mlr_timeout=None):
1572        cmd = 'bbr config'
1573
1574        if seqno is not None:
1575            cmd += ' seqno %d' % seqno
1576
1577        if reg_delay is not None:
1578            cmd += ' delay %d' % reg_delay
1579
1580        if mlr_timeout is not None:
1581            cmd += ' timeout %d' % mlr_timeout
1582
1583        self.send_command(cmd)
1584        self._expect_done()
1585
1586    def set_domain_prefix(self, prefix, flags='prosD'):
1587        self.add_prefix(prefix, flags)
1588        self.register_netdata()
1589
1590    def remove_domain_prefix(self, prefix):
1591        self.remove_prefix(prefix)
1592        self.register_netdata()
1593
1594    def set_next_dua_response(self, status: Union[str, int], iid=None):
1595        # Convert 5.00 to COAP CODE 160
1596        if isinstance(status, str):
1597            assert '.' in status
1598            status = status.split('.')
1599            status = (int(status[0]) << 5) + int(status[1])
1600
1601        cmd = 'bbr mgmt dua {}'.format(status)
1602        if iid is not None:
1603            cmd += ' ' + str(iid)
1604        self.send_command(cmd)
1605        self._expect_done()
1606
1607    def set_dua_iid(self, iid: str):
1608        assert len(iid) == 16
1609        int(iid, 16)
1610
1611        cmd = 'dua iid {}'.format(iid)
1612        self.send_command(cmd)
1613        self._expect_done()
1614
1615    def clear_dua_iid(self):
1616        cmd = 'dua iid clear'
1617        self.send_command(cmd)
1618        self._expect_done()
1619
1620    def multicast_listener_list(self) -> Dict[IPv6Address, int]:
1621        cmd = 'bbr mgmt mlr listener'
1622        self.send_command(cmd)
1623
1624        table = {}
1625        for line in self._expect_results("\S+ \d+"):
1626            line = line.split()
1627            assert len(line) == 2, line
1628            ip = IPv6Address(line[0])
1629            timeout = int(line[1])
1630            assert ip not in table
1631
1632            table[ip] = timeout
1633
1634        return table
1635
1636    def multicast_listener_clear(self):
1637        cmd = f'bbr mgmt mlr listener clear'
1638        self.send_command(cmd)
1639        self._expect_done()
1640
1641    def multicast_listener_add(self, ip: Union[IPv6Address, str], timeout: int = 0):
1642        if not isinstance(ip, IPv6Address):
1643            ip = IPv6Address(ip)
1644
1645        cmd = f'bbr mgmt mlr listener add {ip.compressed} {timeout}'
1646        self.send_command(cmd)
1647        self._expect(r"(Done|Error .*)")
1648
1649    def set_next_mlr_response(self, status: int):
1650        cmd = 'bbr mgmt mlr response {}'.format(status)
1651        self.send_command(cmd)
1652        self._expect_done()
1653
1654    def register_multicast_listener(self, *ipaddrs: Union[IPv6Address, str], timeout=None):
1655        assert len(ipaddrs) > 0, ipaddrs
1656
1657        ipaddrs = map(str, ipaddrs)
1658        cmd = f'mlr reg {" ".join(ipaddrs)}'
1659        if timeout is not None:
1660            cmd += f' {int(timeout)}'
1661        self.send_command(cmd)
1662        self.simulator.go(3)
1663        lines = self._expect_command_output()
1664        m = re.match(r'status (\d+), (\d+) failed', lines[0])
1665        assert m is not None, lines
1666        status = int(m.group(1))
1667        failed_num = int(m.group(2))
1668        assert failed_num == len(lines) - 1
1669        failed_ips = list(map(IPv6Address, lines[1:]))
1670        print(f"register_multicast_listener {ipaddrs} => status: {status}, failed ips: {failed_ips}")
1671        return status, failed_ips
1672
1673    def set_link_quality(self, addr, lqi):
1674        cmd = 'macfilter rss add-lqi %s %s' % (addr, lqi)
1675        self.send_command(cmd)
1676        self._expect_done()
1677
1678    def set_outbound_link_quality(self, lqi):
1679        cmd = 'macfilter rss add-lqi * %s' % (lqi)
1680        self.send_command(cmd)
1681        self._expect_done()
1682
1683    def remove_allowlist(self, addr):
1684        cmd = 'macfilter addr remove %s' % addr
1685        self.send_command(cmd)
1686        self._expect_done()
1687
1688    def get_addr16(self):
1689        self.send_command('rloc16')
1690        rloc16 = self._expect_result(r'[0-9a-fA-F]{4}')
1691        return int(rloc16, 16)
1692
1693    def get_router_id(self):
1694        rloc16 = self.get_addr16()
1695        return rloc16 >> 10
1696
1697    def get_addr64(self):
1698        self.send_command('extaddr')
1699        return self._expect_result('[0-9a-fA-F]{16}')
1700
1701    def set_addr64(self, addr64: str):
1702        # Make sure `addr64` is a hex string of length 16
1703        assert len(addr64) == 16
1704        int(addr64, 16)
1705        self.send_command('extaddr %s' % addr64)
1706        self._expect_done()
1707
1708    def get_eui64(self):
1709        self.send_command('eui64')
1710        return self._expect_result('[0-9a-fA-F]{16}')
1711
1712    def set_extpanid(self, extpanid):
1713        self.send_command('extpanid %s' % extpanid)
1714        self._expect_done()
1715
1716    def get_extpanid(self):
1717        self.send_command('extpanid')
1718        return self._expect_result('[0-9a-fA-F]{16}')
1719
1720    def get_mesh_local_prefix(self):
1721        self.send_command('prefix meshlocal')
1722        return self._expect_command_output()[0]
1723
1724    def set_mesh_local_prefix(self, mesh_local_prefix):
1725        self.send_command('prefix meshlocal %s' % mesh_local_prefix)
1726        self._expect_done()
1727
1728    def get_joiner_id(self):
1729        self.send_command('joiner id')
1730        return self._expect_result('[0-9a-fA-F]{16}')
1731
1732    def get_channel(self):
1733        self.send_command('channel')
1734        return int(self._expect_result(r'\d+'))
1735
1736    def set_channel(self, channel):
1737        cmd = 'channel %d' % channel
1738        self.send_command(cmd)
1739        self._expect_done()
1740
1741    def get_networkkey(self):
1742        self.send_command('networkkey')
1743        return self._expect_result('[0-9a-fA-F]{32}')
1744
1745    def set_networkkey(self, networkkey):
1746        cmd = 'networkkey %s' % networkkey
1747        self.send_command(cmd)
1748        self._expect_done()
1749
1750    def get_key_sequence_counter(self):
1751        self.send_command('keysequence counter')
1752        result = self._expect_result(r'\d+')
1753        return int(result)
1754
1755    def set_key_sequence_counter(self, key_sequence_counter):
1756        cmd = 'keysequence counter %d' % key_sequence_counter
1757        self.send_command(cmd)
1758        self._expect_done()
1759
1760    def get_key_switch_guardtime(self):
1761        self.send_command('keysequence guardtime')
1762        return int(self._expect_result(r'\d+'))
1763
1764    def set_key_switch_guardtime(self, key_switch_guardtime):
1765        cmd = 'keysequence guardtime %d' % key_switch_guardtime
1766        self.send_command(cmd)
1767        self._expect_done()
1768
1769    def set_network_id_timeout(self, network_id_timeout):
1770        cmd = 'networkidtimeout %d' % network_id_timeout
1771        self.send_command(cmd)
1772        self._expect_done()
1773
1774    def _escape_escapable(self, string):
1775        """Escape CLI escapable characters in the given string.
1776
1777        Args:
1778            string (str): UTF-8 input string.
1779
1780        Returns:
1781            [str]: The modified string with escaped characters.
1782        """
1783        escapable_chars = '\\ \t\r\n'
1784        for char in escapable_chars:
1785            string = string.replace(char, '\\%s' % char)
1786        return string
1787
1788    def get_network_name(self):
1789        self.send_command('networkname')
1790        return self._expect_result([r'\S+'])
1791
1792    def set_network_name(self, network_name):
1793        cmd = 'networkname %s' % self._escape_escapable(network_name)
1794        self.send_command(cmd)
1795        self._expect_done()
1796
1797    def get_panid(self):
1798        self.send_command('panid')
1799        result = self._expect_result('0x[0-9a-fA-F]{4}')
1800        return int(result, 16)
1801
1802    def set_panid(self, panid=config.PANID):
1803        cmd = 'panid %d' % panid
1804        self.send_command(cmd)
1805        self._expect_done()
1806
1807    def set_parent_priority(self, priority):
1808        cmd = 'parentpriority %d' % priority
1809        self.send_command(cmd)
1810        self._expect_done()
1811
1812    def get_partition_id(self):
1813        self.send_command('partitionid')
1814        return self._expect_result(r'\d+')
1815
1816    def get_preferred_partition_id(self):
1817        self.send_command('partitionid preferred')
1818        return self._expect_result(r'\d+')
1819
1820    def set_preferred_partition_id(self, partition_id):
1821        cmd = 'partitionid preferred %d' % partition_id
1822        self.send_command(cmd)
1823        self._expect_done()
1824
1825    def get_pollperiod(self):
1826        self.send_command('pollperiod')
1827        return self._expect_result(r'\d+')
1828
1829    def set_pollperiod(self, pollperiod):
1830        self.send_command('pollperiod %d' % pollperiod)
1831        self._expect_done()
1832
1833    def get_child_supervision_interval(self):
1834        self.send_command('childsupervision interval')
1835        return self._expect_result(r'\d+')
1836
1837    def set_child_supervision_interval(self, interval):
1838        self.send_command('childsupervision interval %d' % interval)
1839        self._expect_done()
1840
1841    def get_child_supervision_check_timeout(self):
1842        self.send_command('childsupervision checktimeout')
1843        return self._expect_result(r'\d+')
1844
1845    def set_child_supervision_check_timeout(self, timeout):
1846        self.send_command('childsupervision checktimeout %d' % timeout)
1847        self._expect_done()
1848
1849    def get_child_supervision_check_failure_counter(self):
1850        self.send_command('childsupervision failcounter')
1851        return self._expect_result(r'\d+')
1852
1853    def reset_child_supervision_check_failure_counter(self):
1854        self.send_command('childsupervision failcounter reset')
1855        self._expect_done()
1856
1857    def get_csl_info(self):
1858        self.send_command('csl')
1859        return self._expect_key_value_pairs(r'\S+')
1860
1861    def set_csl_channel(self, csl_channel):
1862        self.send_command('csl channel %d' % csl_channel)
1863        self._expect_done()
1864
1865    def set_csl_period(self, csl_period):
1866        self.send_command('csl period %d' % csl_period)
1867        self._expect_done()
1868
1869    def set_csl_timeout(self, csl_timeout):
1870        self.send_command('csl timeout %d' % csl_timeout)
1871        self._expect_done()
1872
1873    def send_mac_emptydata(self):
1874        self.send_command('mac send emptydata')
1875        self._expect_done()
1876
1877    def send_mac_datarequest(self):
1878        self.send_command('mac send datarequest')
1879        self._expect_done()
1880
1881    def set_router_upgrade_threshold(self, threshold):
1882        cmd = 'routerupgradethreshold %d' % threshold
1883        self.send_command(cmd)
1884        self._expect_done()
1885
1886    def set_router_downgrade_threshold(self, threshold):
1887        cmd = 'routerdowngradethreshold %d' % threshold
1888        self.send_command(cmd)
1889        self._expect_done()
1890
1891    def get_router_downgrade_threshold(self) -> int:
1892        self.send_command('routerdowngradethreshold')
1893        return int(self._expect_result(r'\d+'))
1894
1895    def set_router_eligible(self, enable: bool):
1896        cmd = f'routereligible {"enable" if enable else "disable"}'
1897        self.send_command(cmd)
1898        self._expect_done()
1899
1900    def get_router_eligible(self) -> bool:
1901        states = [r'Disabled', r'Enabled']
1902        self.send_command('routereligible')
1903        return self._expect_result(states) == 'Enabled'
1904
1905    def prefer_router_id(self, router_id):
1906        cmd = 'preferrouterid %d' % router_id
1907        self.send_command(cmd)
1908        self._expect_done()
1909
1910    def release_router_id(self, router_id):
1911        cmd = 'releaserouterid %d' % router_id
1912        self.send_command(cmd)
1913        self._expect_done()
1914
1915    def get_state(self):
1916        states = [r'detached', r'child', r'router', r'leader', r'disabled']
1917        self.send_command('state')
1918        return self._expect_result(states)
1919
1920    def set_state(self, state):
1921        cmd = 'state %s' % state
1922        self.send_command(cmd)
1923        self._expect_done()
1924
1925    def get_ephemeral_key_state(self):
1926        cmd = 'ba ephemeralkey'
1927        states = [r'Disabled', r'Stopped', r'Started', r'Connected', r'Accepted']
1928        self.send_command(cmd)
1929        return self._expect_result(states)
1930
1931    def get_timeout(self):
1932        self.send_command('childtimeout')
1933        return self._expect_result(r'\d+')
1934
1935    def set_timeout(self, timeout):
1936        cmd = 'childtimeout %d' % timeout
1937        self.send_command(cmd)
1938        self._expect_done()
1939
1940    def set_max_children(self, number):
1941        cmd = 'childmax %d' % number
1942        self.send_command(cmd)
1943        self._expect_done()
1944
1945    def get_weight(self):
1946        self.send_command('leaderweight')
1947        return self._expect_result(r'\d+')
1948
1949    def set_weight(self, weight):
1950        cmd = 'leaderweight %d' % weight
1951        self.send_command(cmd)
1952        self._expect_done()
1953
1954    def add_ipaddr(self, ipaddr):
1955        cmd = 'ipaddr add %s' % ipaddr
1956        self.send_command(cmd)
1957        self._expect_done()
1958
1959    def del_ipaddr(self, ipaddr):
1960        cmd = 'ipaddr del %s' % ipaddr
1961        self.send_command(cmd)
1962        self._expect_done()
1963
1964    def add_ipmaddr(self, ipmaddr):
1965        cmd = 'ipmaddr add %s' % ipmaddr
1966        self.send_command(cmd)
1967        self._expect_done()
1968
1969    def del_ipmaddr(self, ipmaddr):
1970        cmd = 'ipmaddr del %s' % ipmaddr
1971        self.send_command(cmd)
1972        self._expect_done()
1973
1974    def get_addrs(self, verbose=False):
1975        self.send_command('ipaddr' + (' -v' if verbose else ''))
1976
1977        return self._expect_results(r'\S+(:\S*)+')
1978
1979    def get_mleid(self):
1980        self.send_command('ipaddr mleid')
1981        return self._expect_result(r'\S+(:\S*)+')
1982
1983    def get_linklocal(self):
1984        self.send_command('ipaddr linklocal')
1985        return self._expect_result(r'\S+(:\S*)+')
1986
1987    def get_rloc(self):
1988        self.send_command('ipaddr rloc')
1989        return self._expect_result(r'\S+(:\S*)+')
1990
1991    def get_addr(self, prefix):
1992        network = ipaddress.ip_network(u'%s' % str(prefix))
1993        addrs = self.get_addrs()
1994
1995        for addr in addrs:
1996            if isinstance(addr, bytearray):
1997                addr = bytes(addr)
1998            ipv6_address = ipaddress.ip_address(addr)
1999            if ipv6_address in network:
2000                return ipv6_address.exploded
2001
2002        return None
2003
2004    def has_ipaddr(self, address):
2005        ipaddr = ipaddress.ip_address(address)
2006        ipaddrs = self.get_addrs()
2007        for addr in ipaddrs:
2008            if isinstance(addr, bytearray):
2009                addr = bytes(addr)
2010            if ipaddress.ip_address(addr) == ipaddr:
2011                return True
2012        return False
2013
2014    def get_ipmaddrs(self):
2015        self.send_command('ipmaddr')
2016        return self._expect_results(r'\S+(:\S*)+')
2017
2018    def has_ipmaddr(self, address):
2019        ipmaddr = ipaddress.ip_address(address)
2020        ipmaddrs = self.get_ipmaddrs()
2021        for addr in ipmaddrs:
2022            if isinstance(addr, bytearray):
2023                addr = bytes(addr)
2024            if ipaddress.ip_address(addr) == ipmaddr:
2025                return True
2026        return False
2027
2028    def get_addr_leader_aloc(self):
2029        addrs = self.get_addrs()
2030        for addr in addrs:
2031            segs = addr.split(':')
2032            if (segs[4] == '0' and segs[5] == 'ff' and segs[6] == 'fe00' and segs[7] == 'fc00'):
2033                return addr
2034        return None
2035
2036    def get_mleid_iid(self):
2037        ml_eid = IPv6Address(self.get_mleid())
2038        return ml_eid.packed[8:].hex()
2039
2040    def get_eidcaches(self):
2041        eidcaches = []
2042        self.send_command('eidcache')
2043        for line in self._expect_results(r'([a-fA-F0-9\:]+) ([a-fA-F0-9]+)'):
2044            eidcaches.append(line.split())
2045
2046        return eidcaches
2047
2048    def add_service(self, enterpriseNumber, serviceData, serverData):
2049        cmd = 'service add %s %s %s' % (
2050            enterpriseNumber,
2051            serviceData,
2052            serverData,
2053        )
2054        self.send_command(cmd)
2055        self._expect_done()
2056
2057    def remove_service(self, enterpriseNumber, serviceData):
2058        cmd = 'service remove %s %s' % (enterpriseNumber, serviceData)
2059        self.send_command(cmd)
2060        self._expect_done()
2061
2062    def get_child_table(self) -> Dict[int, Dict[str, Any]]:
2063        """Get the table of attached children."""
2064        cmd = 'child table'
2065        self.send_command(cmd)
2066        output = self._expect_command_output()
2067
2068        #
2069        # Example output:
2070        # | ID  | RLOC16 | Timeout    | Age        | LQ In | C_VN |R|D|N|Ver|CSL|QMsgCnt|Suprvsn| Extended MAC     |
2071        # +-----+--------+------------+------------+-------+------+-+-+-+---+---+-------+-------+------------------+
2072        # |   1 | 0xc801 |        240 |         24 |     3 |  131 |1|0|0|  3| 0 |     0 |   129 | 4ecede68435358ac |
2073        # |   2 | 0xc802 |        240 |          2 |     3 |  131 |0|0|0|  3| 1 |     0 |     0 | a672a601d2ce37d8 |
2074        # Done
2075        #
2076
2077        headers = self.__split_table_row(output[0])
2078
2079        table = {}
2080        for line in output[2:]:
2081            line = line.strip()
2082            if not line:
2083                continue
2084
2085            fields = self.__split_table_row(line)
2086            col = lambda colname: self.__get_table_col(colname, headers, fields)
2087
2088            id = int(col("ID"))
2089            r, d, n = int(col("R")), int(col("D")), int(col("N"))
2090            mode = f'{"r" if r else ""}{"d" if d else ""}{"n" if n else ""}'
2091
2092            table[int(id)] = {
2093                'id': int(id),
2094                'rloc16': int(col('RLOC16'), 16),
2095                'timeout': int(col('Timeout')),
2096                'age': int(col('Age')),
2097                'lq_in': int(col('LQ In')),
2098                'c_vn': int(col('C_VN')),
2099                'mode': mode,
2100                'extaddr': col('Extended MAC'),
2101                'ver': int(col('Ver')),
2102                'csl': bool(int(col('CSL'))),
2103                'qmsgcnt': int(col('QMsgCnt')),
2104                'suprvsn': int(col('Suprvsn'))
2105            }
2106
2107        return table
2108
2109    def __split_table_row(self, row: str) -> List[str]:
2110        if not (row.startswith('|') and row.endswith('|')):
2111            raise ValueError(row)
2112
2113        fields = row.split('|')
2114        fields = [x.strip() for x in fields[1:-1]]
2115        return fields
2116
2117    def __get_table_col(self, colname: str, headers: List[str], fields: List[str]) -> str:
2118        return fields[headers.index(colname)]
2119
2120    def __getOmrAddress(self):
2121        prefixes = [prefix.split('::')[0] for prefix in self.get_prefixes()]
2122        omr_addrs = []
2123        for addr in self.get_addrs():
2124            for prefix in prefixes:
2125                if (addr.startswith(prefix)) and (addr != self.__getDua()):
2126                    omr_addrs.append(addr)
2127                    break
2128
2129        return omr_addrs
2130
2131    def __getLinkLocalAddress(self):
2132        for ip6Addr in self.get_addrs():
2133            if re.match(config.LINK_LOCAL_REGEX_PATTERN, ip6Addr, re.I):
2134                return ip6Addr
2135
2136        return None
2137
2138    def __getGlobalAddress(self):
2139        global_address = []
2140        for ip6Addr in self.get_addrs():
2141            if ((not re.match(config.LINK_LOCAL_REGEX_PATTERN, ip6Addr, re.I)) and
2142                (not re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr, re.I)) and
2143                (not re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I))):
2144                global_address.append(ip6Addr)
2145
2146        return global_address
2147
2148    def __getRloc(self):
2149        for ip6Addr in self.get_addrs():
2150            if (re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr, re.I) and
2151                    re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I) and
2152                    not (re.match(config.ALOC_FLAG_REGEX_PATTERN, ip6Addr, re.I))):
2153                return ip6Addr
2154        return None
2155
2156    def __getAloc(self):
2157        aloc = []
2158        for ip6Addr in self.get_addrs():
2159            if (re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr, re.I) and
2160                    re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I) and
2161                    re.match(config.ALOC_FLAG_REGEX_PATTERN, ip6Addr, re.I)):
2162                aloc.append(ip6Addr)
2163
2164        return aloc
2165
2166    def __getMleid(self):
2167        for ip6Addr in self.get_addrs():
2168            if re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr,
2169                        re.I) and not (re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I)):
2170                return ip6Addr
2171
2172        return None
2173
2174    def __getDua(self) -> Optional[str]:
2175        for ip6Addr in self.get_addrs():
2176            if re.match(config.DOMAIN_PREFIX_REGEX_PATTERN, ip6Addr, re.I):
2177                return ip6Addr
2178
2179        return None
2180
2181    def get_ip6_address_by_prefix(self, prefix: Union[str, IPv6Network]) -> List[IPv6Address]:
2182        """Get addresses matched with given prefix.
2183
2184        Args:
2185            prefix: the prefix to match against.
2186                    Can be either a string or ipaddress.IPv6Network.
2187
2188        Returns:
2189            The IPv6 address list.
2190        """
2191        if isinstance(prefix, str):
2192            prefix = IPv6Network(prefix)
2193        addrs = map(IPv6Address, self.get_addrs())
2194
2195        return [addr for addr in addrs if addr in prefix]
2196
2197    def get_ip6_address(self, address_type):
2198        """Get specific type of IPv6 address configured on thread device.
2199
2200        Args:
2201            address_type: the config.ADDRESS_TYPE type of IPv6 address.
2202
2203        Returns:
2204            IPv6 address string.
2205        """
2206        if address_type == config.ADDRESS_TYPE.LINK_LOCAL:
2207            return self.__getLinkLocalAddress()
2208        elif address_type == config.ADDRESS_TYPE.GLOBAL:
2209            return self.__getGlobalAddress()
2210        elif address_type == config.ADDRESS_TYPE.RLOC:
2211            return self.__getRloc()
2212        elif address_type == config.ADDRESS_TYPE.ALOC:
2213            return self.__getAloc()
2214        elif address_type == config.ADDRESS_TYPE.ML_EID:
2215            return self.__getMleid()
2216        elif address_type == config.ADDRESS_TYPE.DUA:
2217            return self.__getDua()
2218        elif address_type == config.ADDRESS_TYPE.BACKBONE_GUA:
2219            return self._getBackboneGua()
2220        elif address_type == config.ADDRESS_TYPE.OMR:
2221            return self.__getOmrAddress()
2222        else:
2223            return None
2224
2225    def get_context_reuse_delay(self):
2226        self.send_command('contextreusedelay')
2227        return self._expect_result(r'\d+')
2228
2229    def set_context_reuse_delay(self, delay):
2230        cmd = 'contextreusedelay %d' % delay
2231        self.send_command(cmd)
2232        self._expect_done()
2233
2234    def add_prefix(self, prefix, flags='paosr', prf='med'):
2235        cmd = 'prefix add %s %s %s' % (prefix, flags, prf)
2236        self.send_command(cmd)
2237        self._expect_done()
2238
2239    def remove_prefix(self, prefix):
2240        cmd = 'prefix remove %s' % prefix
2241        self.send_command(cmd)
2242        self._expect_done()
2243
2244    #
2245    # BR commands
2246    #
2247    def enable_br(self):
2248        self.send_command('br enable')
2249        self._expect_done()
2250
2251    def disable_br(self):
2252        self.send_command('br disable')
2253        self._expect_done()
2254
2255    def get_br_omr_prefix(self):
2256        cmd = 'br omrprefix local'
2257        self.send_command(cmd)
2258        return self._expect_command_output()[0]
2259
2260    def get_br_peers(self) -> List[str]:
2261        # Example output of `br peers` command:
2262        #   rloc16:0xa800 age:00:00:50
2263        #   rloc16:0x6800 age:00:00:51
2264        #   Done
2265        self.send_command('br peers')
2266        return self._expect_command_output()
2267
2268    def get_br_peers_rloc16s(self) -> List[int]:
2269        """parse `br peers` output and return the list of RLOC16s"""
2270        return [
2271            int(pair.split(':')[1], 16)
2272            for line in self.get_br_peers()
2273            for pair in line.split()
2274            if pair.split(':')[0] == 'rloc16'
2275        ]
2276
2277    def get_br_routers(self) -> List[str]:
2278        # Example output of `br routers` command:
2279        #   fe80:0:0:0:42:acff:fe14:3 (M:0 O:0 S:1) ms-since-rx:144160 reachable:yes age:00:17:36 (peer BR)
2280        #   fe80:0:0:0:42:acff:fe14:2 (M:0 O:0 S:1) ms-since-rx:45179 reachable:yes age:00:17:36
2281        #   Done
2282        self.send_command('br routers')
2283        return self._expect_command_output()
2284
2285    def get_br_routers_ip_addresses(self) -> List[IPv6Address]:
2286        """parse `br routers` output and return the list of IPv6 addresses"""
2287        return [IPv6Address(line.split()[0]) for line in self.get_br_routers()]
2288
2289    def get_netdata_omr_prefixes(self):
2290        omr_prefixes = []
2291        for prefix in self.get_prefixes():
2292            prefix, flags = prefix.split()[:2]
2293            if 'a' in flags and 'o' in flags and 's' in flags and 'D' not in flags:
2294                omr_prefixes.append(prefix)
2295
2296        return omr_prefixes
2297
2298    def get_br_on_link_prefix(self):
2299        cmd = 'br onlinkprefix local'
2300        self.send_command(cmd)
2301        return self._expect_command_output()[0]
2302
2303    def pd_get_prefix(self):
2304        cmd = 'br pd omrprefix'
2305        self.send_command(cmd)
2306        return self._expect_command_output()[0].split(" ")[0]
2307
2308    def pd_set_enabled(self, enable):
2309        self.send_command('br pd {}'.format("enable" if enable else "disable"))
2310        self._expect_done()
2311
2312    @property
2313    def pd_state(self):
2314        self.send_command('br pd state')
2315        return self._expect_command_output()[0].strip()
2316
2317    def get_netdata_non_nat64_routes(self):
2318        nat64_routes = []
2319        routes = self.get_routes()
2320        for route in routes:
2321            if 'n' not in route.split(' ')[1]:
2322                nat64_routes.append(route.split(' ')[0])
2323        return nat64_routes
2324
2325    def get_netdata_nat64_routes(self):
2326        nat64_routes = []
2327        routes = self.get_routes()
2328        for route in routes:
2329            if 'n' in route.split(' ')[1]:
2330                nat64_routes.append(route.split(' ')[0])
2331        return nat64_routes
2332
2333    def get_br_nat64_prefix(self):
2334        cmd = 'br nat64prefix local'
2335        self.send_command(cmd)
2336        return self._expect_command_output()[0]
2337
2338    def get_br_favored_nat64_prefix(self):
2339        cmd = 'br nat64prefix favored'
2340        self.send_command(cmd)
2341        return self._expect_command_output()[0].split(' ')[0]
2342
2343    def enable_nat64(self):
2344        self.send_command(f'nat64 enable')
2345        self._expect_done()
2346
2347    def disable_nat64(self):
2348        self.send_command(f'nat64 disable')
2349        self._expect_done()
2350
2351    def get_nat64_state(self):
2352        self.send_command('nat64 state')
2353        res = {}
2354        for line in self._expect_command_output():
2355            state = line.split(':')
2356            res[state[0].strip()] = state[1].strip()
2357        return res
2358
2359    def get_nat64_mappings(self):
2360        cmd = 'nat64 mappings'
2361        self.send_command(cmd)
2362        result = self._expect_command_output()
2363        session = None
2364        session_counters = None
2365        sessions = []
2366
2367        for line in result:
2368            m = re.match(
2369                r'\|\s+([a-f0-9]+)\s+\|\s+(.+)\s+\|\s+(.+)\s+\|\s+(\d+)s\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|',
2370                line)
2371            if m:
2372                groups = m.groups()
2373                if session:
2374                    session['counters'] = session_counters
2375                    sessions.append(session)
2376                session = {
2377                    'id': groups[0],
2378                    'ip6': groups[1],
2379                    'ip4': groups[2],
2380                    'expiry': int(groups[3]),
2381                }
2382                session_counters = {}
2383                session_counters['total'] = {
2384                    '4to6': {
2385                        'packets': int(groups[4]),
2386                        'bytes': int(groups[5]),
2387                    },
2388                    '6to4': {
2389                        'packets': int(groups[6]),
2390                        'bytes': int(groups[7]),
2391                    },
2392                }
2393                continue
2394            if not session:
2395                continue
2396            m = re.match(r'\|\s+\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line)
2397            if m:
2398                groups = m.groups()
2399                session_counters[groups[0]] = {
2400                    '4to6': {
2401                        'packets': int(groups[1]),
2402                        'bytes': int(groups[2]),
2403                    },
2404                    '6to4': {
2405                        'packets': int(groups[3]),
2406                        'bytes': int(groups[4]),
2407                    },
2408                }
2409        if session:
2410            session['counters'] = session_counters
2411            sessions.append(session)
2412        return sessions
2413
2414    def get_nat64_counters(self):
2415        cmd = 'nat64 counters'
2416        self.send_command(cmd)
2417        result = self._expect_command_output()
2418
2419        protocol_counters = {}
2420        error_counters = {}
2421        for line in result:
2422            m = re.match(r'\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line)
2423            if m:
2424                groups = m.groups()
2425                protocol_counters[groups[0]] = {
2426                    '4to6': {
2427                        'packets': int(groups[1]),
2428                        'bytes': int(groups[2]),
2429                    },
2430                    '6to4': {
2431                        'packets': int(groups[3]),
2432                        'bytes': int(groups[4]),
2433                    },
2434                }
2435                continue
2436            m = re.match(r'\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line)
2437            if m:
2438                groups = m.groups()
2439                error_counters[groups[0]] = {
2440                    '4to6': {
2441                        'packets': int(groups[1]),
2442                    },
2443                    '6to4': {
2444                        'packets': int(groups[2]),
2445                    },
2446                }
2447                continue
2448        return {'protocol': protocol_counters, 'errors': error_counters}
2449
2450    def get_prefixes(self):
2451        return self.get_netdata()['Prefixes']
2452
2453    def get_routes(self):
2454        return self.get_netdata()['Routes']
2455
2456    def get_services(self):
2457        netdata = self.netdata_show()
2458        services = []
2459        services_section = False
2460
2461        for line in netdata:
2462            if line.startswith('Services:'):
2463                services_section = True
2464            elif line.startswith('Contexts'):
2465                services_section = False
2466            elif services_section:
2467                services.append(line.strip().split(' '))
2468        return services
2469
2470    def netdata_show(self):
2471        self.send_command('netdata show')
2472        return self._expect_command_output()
2473
2474    def get_netdata(self):
2475        raw_netdata = self.netdata_show()
2476        netdata = {'Prefixes': [], 'Routes': [], 'Services': [], 'Contexts': [], 'Commissioning': []}
2477        key_list = ['Prefixes', 'Routes', 'Services', 'Contexts', 'Commissioning']
2478        key = None
2479
2480        for i in range(0, len(raw_netdata)):
2481            keys = list(filter(raw_netdata[i].startswith, key_list))
2482            if keys != []:
2483                key = keys[0]
2484            elif key is not None:
2485                netdata[key].append(raw_netdata[i])
2486
2487        return netdata
2488
2489    def add_route(self, prefix, stable=False, nat64=False, prf='med'):
2490        cmd = 'route add %s ' % prefix
2491        if stable:
2492            cmd += 's'
2493        if nat64:
2494            cmd += 'n'
2495        cmd += ' %s' % prf
2496        self.send_command(cmd)
2497        self._expect_done()
2498
2499    def remove_route(self, prefix):
2500        cmd = 'route remove %s' % prefix
2501        self.send_command(cmd)
2502        self._expect_done()
2503
2504    def register_netdata(self):
2505        self.send_command('netdata register')
2506        self._expect_done()
2507
2508    def netdata_publish_dnssrp_anycast(self, seqnum, version=0):
2509        self.send_command(f'netdata publish dnssrp anycast {seqnum} {version}')
2510        self._expect_done()
2511
2512    def netdata_publish_dnssrp_unicast(self, address, port, version=0):
2513        self.send_command(f'netdata publish dnssrp unicast {address} {port} {version}')
2514        self._expect_done()
2515
2516    def netdata_publish_dnssrp_unicast_mleid(self, port, version=0):
2517        self.send_command(f'netdata publish dnssrp unicast {port} {version}')
2518        self._expect_done()
2519
2520    def netdata_unpublish_dnssrp(self):
2521        self.send_command('netdata unpublish dnssrp')
2522        self._expect_done()
2523
2524    def netdata_publish_prefix(self, prefix, flags='paosr', prf='med'):
2525        self.send_command(f'netdata publish prefix {prefix} {flags} {prf}')
2526        self._expect_done()
2527
2528    def netdata_publish_route(self, prefix, flags='s', prf='med'):
2529        self.send_command(f'netdata publish route {prefix} {flags} {prf}')
2530        self._expect_done()
2531
2532    def netdata_publish_replace(self, old_prefix, prefix, flags='s', prf='med'):
2533        self.send_command(f'netdata publish replace {old_prefix} {prefix} {flags} {prf}')
2534        self._expect_done()
2535
2536    def netdata_unpublish_prefix(self, prefix):
2537        self.send_command(f'netdata unpublish {prefix}')
2538        self._expect_done()
2539
2540    def send_network_diag_get(self, addr, tlv_types):
2541        self.send_command('networkdiagnostic get %s %s' % (addr, ' '.join([str(t.value) for t in tlv_types])))
2542
2543        if isinstance(self.simulator, simulator.VirtualTime):
2544            self.simulator.go(8)
2545            timeout = 1
2546        else:
2547            timeout = 8
2548
2549        self._expect_done(timeout=timeout)
2550
2551    def send_network_diag_reset(self, addr, tlv_types):
2552        self.send_command('networkdiagnostic reset %s %s' % (addr, ' '.join([str(t.value) for t in tlv_types])))
2553
2554        if isinstance(self.simulator, simulator.VirtualTime):
2555            self.simulator.go(8)
2556            timeout = 1
2557        else:
2558            timeout = 8
2559
2560        self._expect_done(timeout=timeout)
2561
2562    def energy_scan(self, mask, count, period, scan_duration, ipaddr):
2563        cmd = 'commissioner energy %d %d %d %d %s' % (
2564            mask,
2565            count,
2566            period,
2567            scan_duration,
2568            ipaddr,
2569        )
2570        self.send_command(cmd)
2571
2572        if isinstance(self.simulator, simulator.VirtualTime):
2573            self.simulator.go(8)
2574            timeout = 1
2575        else:
2576            timeout = 8
2577
2578        self._expect('Energy:', timeout=timeout)
2579
2580    def panid_query(self, panid, mask, ipaddr):
2581        cmd = 'commissioner panid %d %d %s' % (panid, mask, ipaddr)
2582        self.send_command(cmd)
2583
2584        if isinstance(self.simulator, simulator.VirtualTime):
2585            self.simulator.go(8)
2586            timeout = 1
2587        else:
2588            timeout = 8
2589
2590        self._expect('Conflict:', timeout=timeout)
2591
2592    def scan(self, result=1, timeout=10):
2593        self.send_command('scan')
2594
2595        self.simulator.go(timeout)
2596
2597        if result == 1:
2598            networks = []
2599            for line in self._expect_command_output()[2:]:
2600                _, panid, extaddr, channel, dbm, lqi, _ = map(str.strip, line.split('|'))
2601                panid = int(panid, 16)
2602                channel, dbm, lqi = map(int, (channel, dbm, lqi))
2603
2604                networks.append({
2605                    'panid': panid,
2606                    'extaddr': extaddr,
2607                    'channel': channel,
2608                    'dbm': dbm,
2609                    'lqi': lqi,
2610                })
2611            return networks
2612
2613    def scan_energy(self, timeout=10):
2614        self.send_command('scan energy')
2615        self.simulator.go(timeout)
2616        rssi_list = []
2617        for line in self._expect_command_output()[2:]:
2618            _, channel, rssi, _ = line.split('|')
2619            rssi_list.append({
2620                'channel': int(channel.strip()),
2621                'rssi': int(rssi.strip()),
2622            })
2623        return rssi_list
2624
2625    def ping(self, ipaddr, num_responses=1, size=8, timeout=5, count=1, interval=1, hoplimit=64, interface=None):
2626        args = f'{ipaddr} {size} {count} {interval} {hoplimit} {timeout}'
2627        if interface is not None:
2628            args = f'-I {interface} {args}'
2629        cmd = f'ping {args}'
2630
2631        self.send_command(cmd)
2632
2633        wait_allowance = 3
2634        end = self.simulator.now() + timeout + wait_allowance
2635
2636        responders = {}
2637
2638        result = True
2639        # ncp-sim doesn't print Done
2640        done = (self.node_type == 'ncp-sim')
2641        while len(responders) < num_responses or not done:
2642            self.simulator.go(1)
2643            try:
2644                i = self._expect([r'from (\S+):', r'Done'], timeout=0.1)
2645            except (pexpect.TIMEOUT, socket.timeout):
2646                if self.simulator.now() < end:
2647                    continue
2648                result = False
2649                if isinstance(self.simulator, simulator.VirtualTime):
2650                    self.simulator.sync_devices()
2651                break
2652            else:
2653                if i == 0:
2654                    responders[self.pexpect.match.groups()[0]] = 1
2655                elif i == 1:
2656                    done = True
2657        return result
2658
2659    def reset(self):
2660        self._reset('reset')
2661
2662    def factory_reset(self):
2663        self._reset('factoryreset')
2664
2665    def _reset(self, cmd):
2666        self.send_command(cmd, expect_command_echo=False)
2667        time.sleep(self.RESET_DELAY)
2668        # Send a "version" command and drain the CLI output after reset
2669        self.send_command('version', expect_command_echo=False)
2670        while True:
2671            try:
2672                self._expect(r"[^\n]+\n", timeout=0.1)
2673                continue
2674            except pexpect.TIMEOUT:
2675                break
2676
2677        if self.is_otbr:
2678            self.set_log_level(5)
2679
2680    def set_router_selection_jitter(self, jitter):
2681        cmd = 'routerselectionjitter %d' % jitter
2682        self.send_command(cmd)
2683        self._expect_done()
2684
2685    def set_active_dataset(
2686        self,
2687        timestamp=None,
2688        channel=None,
2689        channel_mask=None,
2690        extended_panid=None,
2691        mesh_local_prefix=None,
2692        network_key=None,
2693        network_name=None,
2694        panid=None,
2695        pskc=None,
2696        security_policy=[],
2697        updateExisting=False,
2698    ):
2699
2700        if updateExisting:
2701            self.send_command('dataset init active', go=False)
2702        else:
2703            self.send_command('dataset clear', go=False)
2704        self._expect_done()
2705
2706        if timestamp is not None:
2707            cmd = 'dataset activetimestamp %d' % timestamp
2708            self.send_command(cmd, go=False)
2709            self._expect_done()
2710
2711        if channel is not None:
2712            cmd = 'dataset channel %d' % channel
2713            self.send_command(cmd, go=False)
2714            self._expect_done()
2715
2716        if channel_mask is not None:
2717            cmd = 'dataset channelmask %d' % channel_mask
2718            self.send_command(cmd, go=False)
2719            self._expect_done()
2720
2721        if extended_panid is not None:
2722            cmd = 'dataset extpanid %s' % extended_panid
2723            self.send_command(cmd, go=False)
2724            self._expect_done()
2725
2726        if mesh_local_prefix is not None:
2727            cmd = 'dataset meshlocalprefix %s' % mesh_local_prefix
2728            self.send_command(cmd, go=False)
2729            self._expect_done()
2730
2731        if network_key is not None:
2732            cmd = 'dataset networkkey %s' % network_key
2733            self.send_command(cmd, go=False)
2734            self._expect_done()
2735
2736        if network_name is not None:
2737            cmd = 'dataset networkname %s' % network_name
2738            self.send_command(cmd, go=False)
2739            self._expect_done()
2740
2741        if panid is not None:
2742            cmd = 'dataset panid %d' % panid
2743            self.send_command(cmd, go=False)
2744            self._expect_done()
2745
2746        if pskc is not None:
2747            cmd = 'dataset pskc %s' % pskc
2748            self.send_command(cmd, go=False)
2749            self._expect_done()
2750
2751        if security_policy is not None:
2752            if len(security_policy) >= 2:
2753                cmd = 'dataset securitypolicy %s %s' % (
2754                    str(security_policy[0]),
2755                    security_policy[1],
2756                )
2757            if len(security_policy) >= 3:
2758                cmd += ' %s' % (str(security_policy[2]))
2759            self.send_command(cmd, go=False)
2760            self._expect_done()
2761
2762        self.send_command('dataset commit active', go=False)
2763        self._expect_done()
2764
2765    def set_pending_dataset(self, pendingtimestamp, activetimestamp, panid=None, channel=None, delay=None):
2766        self.send_command('dataset clear')
2767        self._expect_done()
2768
2769        cmd = 'dataset pendingtimestamp %d' % pendingtimestamp
2770        self.send_command(cmd)
2771        self._expect_done()
2772
2773        cmd = 'dataset activetimestamp %d' % activetimestamp
2774        self.send_command(cmd)
2775        self._expect_done()
2776
2777        if panid is not None:
2778            cmd = 'dataset panid %d' % panid
2779            self.send_command(cmd)
2780            self._expect_done()
2781
2782        if channel is not None:
2783            cmd = 'dataset channel %d' % channel
2784            self.send_command(cmd)
2785            self._expect_done()
2786
2787        if delay is not None:
2788            cmd = 'dataset delay %d' % delay
2789            self.send_command(cmd)
2790            self._expect_done()
2791
2792        # Set the meshlocal prefix in config.py
2793        self.send_command('dataset meshlocalprefix %s' % config.MESH_LOCAL_PREFIX.split('/')[0])
2794        self._expect_done()
2795
2796        self.send_command('dataset commit pending')
2797        self._expect_done()
2798
2799    def start_dataset_updater(self, panid=None, channel=None, security_policy=None, delay=None):
2800        self.send_command('dataset clear')
2801        self._expect_done()
2802
2803        if panid is not None:
2804            cmd = 'dataset panid %d' % panid
2805            self.send_command(cmd)
2806            self._expect_done()
2807
2808        if channel is not None:
2809            cmd = 'dataset channel %d' % channel
2810            self.send_command(cmd)
2811            self._expect_done()
2812
2813        if security_policy is not None:
2814            cmd = 'dataset securitypolicy %d %s ' % (security_policy[0], security_policy[1])
2815            if (len(security_policy) >= 3):
2816                cmd += '%d ' % (security_policy[2])
2817            self.send_command(cmd)
2818            self._expect_done()
2819
2820        if delay is not None:
2821            cmd = 'dataset delay %d ' % delay
2822            self.send_command(cmd)
2823            self._expect_done()
2824
2825        self.send_command('dataset updater start')
2826        self._expect_done()
2827
2828    def announce_begin(self, mask, count, period, ipaddr):
2829        cmd = 'commissioner announce %d %d %d %s' % (
2830            mask,
2831            count,
2832            period,
2833            ipaddr,
2834        )
2835        self.send_command(cmd)
2836        self._expect_done()
2837
2838    def send_mgmt_active_set(
2839        self,
2840        active_timestamp=None,
2841        channel=None,
2842        channel_mask=None,
2843        extended_panid=None,
2844        panid=None,
2845        network_key=None,
2846        mesh_local=None,
2847        network_name=None,
2848        security_policy=None,
2849        binary=None,
2850    ):
2851        cmd = 'dataset mgmtsetcommand active '
2852
2853        if active_timestamp is not None:
2854            cmd += 'activetimestamp %d ' % active_timestamp
2855
2856        if channel is not None:
2857            cmd += 'channel %d ' % channel
2858
2859        if channel_mask is not None:
2860            cmd += 'channelmask %d ' % channel_mask
2861
2862        if extended_panid is not None:
2863            cmd += 'extpanid %s ' % extended_panid
2864
2865        if panid is not None:
2866            cmd += 'panid %d ' % panid
2867
2868        if network_key is not None:
2869            cmd += 'networkkey %s ' % network_key
2870
2871        if mesh_local is not None:
2872            cmd += 'localprefix %s ' % mesh_local
2873
2874        if network_name is not None:
2875            cmd += 'networkname %s ' % self._escape_escapable(network_name)
2876
2877        if security_policy is not None:
2878            cmd += 'securitypolicy %d %s ' % (security_policy[0], security_policy[1])
2879            if (len(security_policy) >= 3):
2880                cmd += '%d ' % (security_policy[2])
2881
2882        if binary is not None:
2883            cmd += '-x %s ' % binary
2884
2885        self.send_command(cmd)
2886        self._expect_done()
2887
2888    def send_mgmt_active_get(self, addr='', tlvs=[]):
2889        cmd = 'dataset mgmtgetcommand active'
2890
2891        if addr != '':
2892            cmd += ' address '
2893            cmd += addr
2894
2895        if len(tlvs) != 0:
2896            tlv_str = ''.join('%02x' % tlv for tlv in tlvs)
2897            cmd += ' -x '
2898            cmd += tlv_str
2899
2900        self.send_command(cmd)
2901        self._expect_done()
2902
2903    def send_mgmt_pending_get(self, addr='', tlvs=[]):
2904        cmd = 'dataset mgmtgetcommand pending'
2905
2906        if addr != '':
2907            cmd += ' address '
2908            cmd += addr
2909
2910        if len(tlvs) != 0:
2911            tlv_str = ''.join('%02x' % tlv for tlv in tlvs)
2912            cmd += ' -x '
2913            cmd += tlv_str
2914
2915        self.send_command(cmd)
2916        self._expect_done()
2917
2918    def send_mgmt_pending_set(
2919        self,
2920        pending_timestamp=None,
2921        active_timestamp=None,
2922        delay_timer=None,
2923        channel=None,
2924        panid=None,
2925        network_key=None,
2926        mesh_local=None,
2927        network_name=None,
2928    ):
2929        cmd = 'dataset mgmtsetcommand pending '
2930        if pending_timestamp is not None:
2931            cmd += 'pendingtimestamp %d ' % pending_timestamp
2932
2933        if active_timestamp is not None:
2934            cmd += 'activetimestamp %d ' % active_timestamp
2935
2936        if delay_timer is not None:
2937            cmd += 'delaytimer %d ' % delay_timer
2938
2939        if channel is not None:
2940            cmd += 'channel %d ' % channel
2941
2942        if panid is not None:
2943            cmd += 'panid %d ' % panid
2944
2945        if network_key is not None:
2946            cmd += 'networkkey %s ' % network_key
2947
2948        if mesh_local is not None:
2949            cmd += 'localprefix %s ' % mesh_local
2950
2951        if network_name is not None:
2952            cmd += 'networkname %s ' % self._escape_escapable(network_name)
2953
2954        self.send_command(cmd)
2955        self._expect_done()
2956
2957    def coap_cancel(self):
2958        """
2959        Cancel a CoAP subscription.
2960        """
2961        cmd = 'coap cancel'
2962        self.send_command(cmd)
2963        self._expect_done()
2964
2965    def coap_delete(self, ipaddr, uri, con=False, payload=None):
2966        """
2967        Send a DELETE request via CoAP.
2968        """
2969        return self._coap_rq('delete', ipaddr, uri, con, payload)
2970
2971    def coap_get(self, ipaddr, uri, con=False, payload=None):
2972        """
2973        Send a GET request via CoAP.
2974        """
2975        return self._coap_rq('get', ipaddr, uri, con, payload)
2976
2977    def coap_get_block(self, ipaddr, uri, size=16, count=0):
2978        """
2979        Send a GET request via CoAP.
2980        """
2981        return self._coap_rq_block('get', ipaddr, uri, size, count)
2982
2983    def coap_observe(self, ipaddr, uri, con=False, payload=None):
2984        """
2985        Send a GET request via CoAP with Observe set.
2986        """
2987        return self._coap_rq('observe', ipaddr, uri, con, payload)
2988
2989    def coap_post(self, ipaddr, uri, con=False, payload=None):
2990        """
2991        Send a POST request via CoAP.
2992        """
2993        return self._coap_rq('post', ipaddr, uri, con, payload)
2994
2995    def coap_post_block(self, ipaddr, uri, size=16, count=0):
2996        """
2997        Send a POST request via CoAP.
2998        """
2999        return self._coap_rq_block('post', ipaddr, uri, size, count)
3000
3001    def coap_put(self, ipaddr, uri, con=False, payload=None):
3002        """
3003        Send a PUT request via CoAP.
3004        """
3005        return self._coap_rq('put', ipaddr, uri, con, payload)
3006
3007    def coap_put_block(self, ipaddr, uri, size=16, count=0):
3008        """
3009        Send a PUT request via CoAP.
3010        """
3011        return self._coap_rq_block('put', ipaddr, uri, size, count)
3012
3013    def _coap_rq(self, method, ipaddr, uri, con=False, payload=None):
3014        """
3015        Issue a GET/POST/PUT/DELETE/GET OBSERVE request.
3016        """
3017        cmd = 'coap %s %s %s' % (method, ipaddr, uri)
3018        if con:
3019            cmd += ' con'
3020        else:
3021            cmd += ' non'
3022
3023        if payload is not None:
3024            cmd += ' %s' % payload
3025
3026        self.send_command(cmd)
3027        return self.coap_wait_response()
3028
3029    def _coap_rq_block(self, method, ipaddr, uri, size=16, count=0):
3030        """
3031        Issue a GET/POST/PUT/DELETE/GET OBSERVE BLOCK request.
3032        """
3033        cmd = 'coap %s %s %s' % (method, ipaddr, uri)
3034
3035        cmd += ' block-%d' % size
3036
3037        if count != 0:
3038            cmd += ' %d' % count
3039
3040        self.send_command(cmd)
3041        return self.coap_wait_response()
3042
3043    def coap_wait_response(self):
3044        """
3045        Wait for a CoAP response, and return it.
3046        """
3047        if isinstance(self.simulator, simulator.VirtualTime):
3048            self.simulator.go(5)
3049            timeout = 1
3050        else:
3051            timeout = 5
3052
3053        self._expect(r'coap response from ([\da-f:]+)(?: OBS=(\d+))?'
3054                     r'(?: with payload: ([\da-f]+))?\b',
3055                     timeout=timeout)
3056        (source, observe, payload) = self.pexpect.match.groups()
3057        source = source.decode('UTF-8')
3058
3059        if observe is not None:
3060            observe = int(observe, base=10)
3061
3062        if payload is not None:
3063            try:
3064                payload = binascii.a2b_hex(payload).decode('UTF-8')
3065            except UnicodeDecodeError:
3066                pass
3067
3068        # Return the values received
3069        return dict(source=source, observe=observe, payload=payload)
3070
3071    def coap_wait_request(self):
3072        """
3073        Wait for a CoAP request to be made.
3074        """
3075        if isinstance(self.simulator, simulator.VirtualTime):
3076            self.simulator.go(5)
3077            timeout = 1
3078        else:
3079            timeout = 5
3080
3081        self._expect(r'coap request from ([\da-f:]+)(?: OBS=(\d+))?'
3082                     r'(?: with payload: ([\da-f]+))?\b',
3083                     timeout=timeout)
3084        (source, observe, payload) = self.pexpect.match.groups()
3085        source = source.decode('UTF-8')
3086
3087        if observe is not None:
3088            observe = int(observe, base=10)
3089
3090        if payload is not None:
3091            payload = binascii.a2b_hex(payload).decode('UTF-8')
3092
3093        # Return the values received
3094        return dict(source=source, observe=observe, payload=payload)
3095
3096    def coap_wait_subscribe(self):
3097        """
3098        Wait for a CoAP client to be subscribed.
3099        """
3100        if isinstance(self.simulator, simulator.VirtualTime):
3101            self.simulator.go(5)
3102            timeout = 1
3103        else:
3104            timeout = 5
3105
3106        self._expect(r'Subscribing client\b', timeout=timeout)
3107
3108    def coap_wait_ack(self):
3109        """
3110        Wait for a CoAP notification ACK.
3111        """
3112        if isinstance(self.simulator, simulator.VirtualTime):
3113            self.simulator.go(5)
3114            timeout = 1
3115        else:
3116            timeout = 5
3117
3118        self._expect(r'Received ACK in reply to notification from ([\da-f:]+)\b', timeout=timeout)
3119        (source,) = self.pexpect.match.groups()
3120        source = source.decode('UTF-8')
3121
3122        return source
3123
3124    def coap_set_resource_path(self, path):
3125        """
3126        Set the path for the CoAP resource.
3127        """
3128        cmd = 'coap resource %s' % path
3129        self.send_command(cmd)
3130        self._expect_done()
3131
3132    def coap_set_resource_path_block(self, path, count=0):
3133        """
3134        Set the path for the CoAP resource and how many blocks can be received from this resource.
3135        """
3136        cmd = 'coap resource %s %d' % (path, count)
3137        self.send_command(cmd)
3138        self._expect('Done')
3139
3140    def coap_set_content(self, content):
3141        """
3142        Set the content of the CoAP resource.
3143        """
3144        cmd = 'coap set %s' % content
3145        self.send_command(cmd)
3146        self._expect_done()
3147
3148    def coap_start(self):
3149        """
3150        Start the CoAP service.
3151        """
3152        cmd = 'coap start'
3153        self.send_command(cmd)
3154        self._expect_done()
3155
3156    def coap_stop(self):
3157        """
3158        Stop the CoAP service.
3159        """
3160        cmd = 'coap stop'
3161        self.send_command(cmd)
3162
3163        if isinstance(self.simulator, simulator.VirtualTime):
3164            self.simulator.go(5)
3165            timeout = 1
3166        else:
3167            timeout = 5
3168
3169        self._expect_done(timeout=timeout)
3170
3171    def coaps_start_psk(self, psk, pskIdentity):
3172        cmd = 'coaps psk %s %s' % (psk, pskIdentity)
3173        self.send_command(cmd)
3174        self._expect_done()
3175
3176        cmd = 'coaps start'
3177        self.send_command(cmd)
3178        self._expect_done()
3179
3180    def coaps_start_x509(self):
3181        cmd = 'coaps x509'
3182        self.send_command(cmd)
3183        self._expect_done()
3184
3185        cmd = 'coaps start'
3186        self.send_command(cmd)
3187        self._expect_done()
3188
3189    def coaps_set_resource_path(self, path):
3190        cmd = 'coaps resource %s' % path
3191        self.send_command(cmd)
3192        self._expect_done()
3193
3194    def coaps_stop(self):
3195        cmd = 'coaps stop'
3196        self.send_command(cmd)
3197
3198        if isinstance(self.simulator, simulator.VirtualTime):
3199            self.simulator.go(5)
3200            timeout = 1
3201        else:
3202            timeout = 5
3203
3204        self._expect_done(timeout=timeout)
3205
3206    def coaps_connect(self, ipaddr):
3207        cmd = 'coaps connect %s' % ipaddr
3208        self.send_command(cmd)
3209
3210        if isinstance(self.simulator, simulator.VirtualTime):
3211            self.simulator.go(5)
3212            timeout = 1
3213        else:
3214            timeout = 5
3215
3216        self._expect('coaps connected', timeout=timeout)
3217
3218    def coaps_disconnect(self):
3219        cmd = 'coaps disconnect'
3220        self.send_command(cmd)
3221        self._expect_done()
3222        self.simulator.go(5)
3223
3224    def coaps_get(self):
3225        cmd = 'coaps get test'
3226        self.send_command(cmd)
3227
3228        if isinstance(self.simulator, simulator.VirtualTime):
3229            self.simulator.go(5)
3230            timeout = 1
3231        else:
3232            timeout = 5
3233
3234        self._expect('coaps response', timeout=timeout)
3235
3236    def commissioner_mgmtget(self, tlvs_binary=None):
3237        cmd = 'commissioner mgmtget'
3238        if tlvs_binary is not None:
3239            cmd += ' -x %s' % tlvs_binary
3240        self.send_command(cmd)
3241        self._expect_done()
3242
3243    def commissioner_mgmtset(self, tlvs_binary):
3244        cmd = 'commissioner mgmtset -x %s' % tlvs_binary
3245        self.send_command(cmd)
3246        self._expect_done()
3247
3248    def bytes_to_hex_str(self, src):
3249        return ''.join(format(x, '02x') for x in src)
3250
3251    def commissioner_mgmtset_with_tlvs(self, tlvs):
3252        payload = bytearray()
3253        for tlv in tlvs:
3254            payload += tlv.to_hex()
3255        self.commissioner_mgmtset(self.bytes_to_hex_str(payload))
3256
3257    def udp_start(self, local_ipaddr, local_port, bind_unspecified=False):
3258        cmd = 'udp open'
3259        self.send_command(cmd)
3260        self._expect_done()
3261
3262        cmd = 'udp bind %s %s %s' % ("-u" if bind_unspecified else "", local_ipaddr, local_port)
3263        self.send_command(cmd)
3264        self._expect_done()
3265
3266    def udp_stop(self):
3267        cmd = 'udp close'
3268        self.send_command(cmd)
3269        self._expect_done()
3270
3271    def udp_send(self, bytes, ipaddr, port, success=True):
3272        cmd = 'udp send %s %d -s %d ' % (ipaddr, port, bytes)
3273        self.send_command(cmd)
3274        if success:
3275            self._expect_done()
3276        else:
3277            self._expect('Error')
3278
3279    def udp_check_rx(self, bytes_should_rx):
3280        self._expect('%d bytes' % bytes_should_rx)
3281
3282    def set_routereligible(self, enable: bool):
3283        cmd = f'routereligible {"enable" if enable else "disable"}'
3284        self.send_command(cmd)
3285        self._expect_done()
3286
3287    def router_list(self):
3288        cmd = 'router list'
3289        self.send_command(cmd)
3290        self._expect([r'(\d+)((\s\d+)*)'])
3291
3292        g = self.pexpect.match.groups()
3293        router_list = g[0].decode('utf8') + ' ' + g[1].decode('utf8')
3294        router_list = [int(x) for x in router_list.split()]
3295        self._expect_done()
3296        return router_list
3297
3298    def router_table(self):
3299        cmd = 'router table'
3300        self.send_command(cmd)
3301
3302        self._expect(r'(.*)Done')
3303        g = self.pexpect.match.groups()
3304        output = g[0].decode('utf8')
3305        lines = output.strip().split('\n')
3306        lines = [l.strip() for l in lines]
3307        router_table = {}
3308        for i, line in enumerate(lines):
3309            if not line.startswith('|') or not line.endswith('|'):
3310                if i not in (0, 2):
3311                    # should not happen
3312                    print("unexpected line %d: %s" % (i, line))
3313
3314                continue
3315
3316            line = line[1:][:-1]
3317            line = [x.strip() for x in line.split('|')]
3318            if len(line) < 9:
3319                print("unexpected line %d: %s" % (i, line))
3320                continue
3321
3322            try:
3323                int(line[0])
3324            except ValueError:
3325                if i != 1:
3326                    print("unexpected line %d: %s" % (i, line))
3327                continue
3328
3329            id = int(line[0])
3330            rloc16 = int(line[1], 16)
3331            nexthop = int(line[2])
3332            pathcost = int(line[3])
3333            lqin = int(line[4])
3334            lqout = int(line[5])
3335            age = int(line[6])
3336            emac = str(line[7])
3337            link = int(line[8])
3338
3339            router_table[id] = {
3340                'rloc16': rloc16,
3341                'nexthop': nexthop,
3342                'pathcost': pathcost,
3343                'lqin': lqin,
3344                'lqout': lqout,
3345                'age': age,
3346                'emac': emac,
3347                'link': link,
3348            }
3349
3350        return router_table
3351
3352    def link_metrics_request_single_probe(self, dst_addr: str, linkmetrics_flags: str, mode: str = ''):
3353        cmd = 'linkmetrics request %s %s single %s' % (mode, dst_addr, linkmetrics_flags)
3354        self.send_command(cmd)
3355        self.simulator.go(5)
3356        return self._parse_linkmetrics_query_result(self._expect_command_output())
3357
3358    def link_metrics_request_forward_tracking_series(self, dst_addr: str, series_id: int, mode: str = ''):
3359        cmd = 'linkmetrics request %s %s forward %d' % (mode, dst_addr, series_id)
3360        self.send_command(cmd)
3361        self.simulator.go(5)
3362        return self._parse_linkmetrics_query_result(self._expect_command_output())
3363
3364    def _parse_linkmetrics_query_result(self, lines):
3365        """Parse link metrics query result"""
3366
3367        # Example of command output:
3368        # ['Received Link Metrics Report from: fe80:0:0:0:146e:a00:0:1',
3369        #  '- PDU Counter: 1 (Count/Summation)',
3370        #  '- LQI: 0 (Exponential Moving Average)',
3371        #  '- Margin: 80 (dB) (Exponential Moving Average)',
3372        #  '- RSSI: -20 (dBm) (Exponential Moving Average)']
3373        #
3374        # Or 'Link Metrics Report, status: {status}'
3375
3376        result = {}
3377        for line in lines:
3378            if line.startswith('- '):
3379                k, v = line[2:].split(': ')
3380                result[k] = v.split(' ')[0]
3381            elif line.startswith('Link Metrics Report, status: '):
3382                result['Status'] = line[29:]
3383        return result
3384
3385    def link_metrics_config_req_enhanced_ack_based_probing(self,
3386                                                           dst_addr: str,
3387                                                           enable: bool,
3388                                                           metrics_flags: str,
3389                                                           ext_flags='',
3390                                                           mode: str = ''):
3391        cmd = "linkmetrics config %s %s enhanced-ack" % (mode, dst_addr)
3392        if enable:
3393            cmd = cmd + (" register %s %s" % (metrics_flags, ext_flags))
3394        else:
3395            cmd = cmd + " clear"
3396        self.send_command(cmd)
3397        self._expect_done()
3398
3399    def link_metrics_config_req_forward_tracking_series(self,
3400                                                        dst_addr: str,
3401                                                        series_id: int,
3402                                                        series_flags: str,
3403                                                        metrics_flags: str,
3404                                                        mode: str = ''):
3405        cmd = "linkmetrics config %s %s forward %d %s %s" % (mode, dst_addr, series_id, series_flags, metrics_flags)
3406        self.send_command(cmd)
3407        self._expect_done()
3408
3409    def link_metrics_send_link_probe(self, dst_addr: str, series_id: int, length: int):
3410        cmd = "linkmetrics probe %s %d %d" % (dst_addr, series_id, length)
3411        self.send_command(cmd)
3412        self._expect_done()
3413
3414    def link_metrics_mgr_set_enabled(self, enable: bool):
3415        op_str = "enable" if enable else "disable"
3416        cmd = f'linkmetricsmgr {op_str}'
3417        self.send_command(cmd)
3418        self._expect_done()
3419
3420    def send_address_notification(self, dst: str, target: str, mliid: str):
3421        cmd = f'fake /a/an {dst} {target} {mliid}'
3422        self.send_command(cmd)
3423        self._expect_done()
3424
3425    def send_proactive_backbone_notification(self, target: str, mliid: str, ltt: int):
3426        cmd = f'fake /b/ba {target} {mliid} {ltt}'
3427        self.send_command(cmd)
3428        self._expect_done()
3429
3430    def dns_get_config(self):
3431        """
3432        Returns the DNS config as a list of property dictionary (string key and string value).
3433
3434        Example output:
3435        {
3436            'Server': '[fd00:0:0:0:0:0:0:1]:1234'
3437            'ResponseTimeout': '5000 ms'
3438            'MaxTxAttempts': '2'
3439            'RecursionDesired': 'no'
3440        }
3441        """
3442        cmd = f'dns config'
3443        self.send_command(cmd)
3444        output = self._expect_command_output()
3445        config = {}
3446        for line in output:
3447            k, v = line.split(': ')
3448            config[k] = v
3449        return config
3450
3451    def dns_set_config(self, config):
3452        cmd = f'dns config {config}'
3453        self.send_command(cmd)
3454        self._expect_done()
3455
3456    def dns_resolve(self, hostname, server=None, port=53):
3457        cmd = f'dns resolve {hostname}'
3458        if server is not None:
3459            cmd += f' {server} {port}'
3460
3461        self.send_command(cmd)
3462        self.simulator.go(10)
3463        output = self._expect_command_output()
3464        dns_resp = output[0]
3465        # example output: "DNS response for host1.default.service.arpa. - fd00:db8:0:0:fd3d:d471:1e8c:b60 TTL:7190 "
3466        #                 " fd00:db8:0:0:0:ff:fe00:9000 TTL:7190"
3467        addrs = dns_resp.strip().split(' - ')[1].split(' ')
3468        ip = [item.strip() for item in addrs[::2]]
3469        ttl = [int(item.split('TTL:')[1]) for item in addrs[1::2]]
3470
3471        return list(zip(ip, ttl))
3472
3473    def _parse_dns_service_info(self, output):
3474        # Example of `output`
3475        #   Port:22222, Priority:2, Weight:2, TTL:7155
3476        #   Host:host2.default.service.arpa.
3477        #   HostAddress:0:0:0:0:0:0:0:0 TTL:0
3478        #   TXT:[a=00, b=02bb] TTL:7155
3479
3480        m = re.match(
3481            r'.*Port:(\d+), Priority:(\d+), Weight:(\d+), TTL:(\d+)\s+Host:(.*?)\s+HostAddress:(\S+) TTL:(\d+)\s+TXT:\[(.*?)\] TTL:(\d+)',
3482            '\r'.join(output))
3483        if not m:
3484            return {}
3485        port, priority, weight, srv_ttl, hostname, address, aaaa_ttl, txt_data, txt_ttl = m.groups()
3486        return {
3487            'port': int(port),
3488            'priority': int(priority),
3489            'weight': int(weight),
3490            'host': hostname,
3491            'address': address,
3492            'txt_data': txt_data,
3493            'srv_ttl': int(srv_ttl),
3494            'txt_ttl': int(txt_ttl),
3495            'aaaa_ttl': int(aaaa_ttl),
3496        }
3497
3498    def dns_resolve_service(self, instance, service, server=None, port=53):
3499        """
3500        Resolves the service instance and returns the instance information as a dict.
3501
3502        Example return value:
3503            {
3504                'port': 12345,
3505                'priority': 0,
3506                'weight': 0,
3507                'host': 'ins1._ipps._tcp.default.service.arpa.',
3508                'address': '2001::1',
3509                'txt_data': 'a=00, b=02bb',
3510                'srv_ttl': 7100,
3511                'txt_ttl': 7100,
3512                'aaaa_ttl': 7100,
3513            }
3514        """
3515        instance = self._escape_escapable(instance)
3516        cmd = f'dns service {instance} {service}'
3517        if server is not None:
3518            cmd += f' {server} {port}'
3519
3520        self.send_command(cmd)
3521        self.simulator.go(10)
3522        output = self._expect_command_output()
3523        info = self._parse_dns_service_info(output)
3524        if not info:
3525            raise Exception('dns resolve service failed: %s.%s' % (instance, service))
3526        return info
3527
3528    @staticmethod
3529    def __parse_hex_string(hexstr: str) -> bytes:
3530        assert (len(hexstr) % 2 == 0)
3531        return bytes(int(hexstr[i:i + 2], 16) for i in range(0, len(hexstr), 2))
3532
3533    def dns_browse(self, service_name, server=None, port=53):
3534        """
3535        Browse the service and returns the instances.
3536
3537        Example return value:
3538            {
3539                'ins1': {
3540                    'port': 12345,
3541                    'priority': 1,
3542                    'weight': 1,
3543                    'host': 'ins1._ipps._tcp.default.service.arpa.',
3544                    'address': '2001::1',
3545                    'txt_data': 'a=00, b=11cf',
3546                    'srv_ttl': 7100,
3547                    'txt_ttl': 7100,
3548                    'aaaa_ttl': 7100,
3549                },
3550                'ins2': {
3551                    'port': 12345,
3552                    'priority': 2,
3553                    'weight': 2,
3554                    'host': 'ins2._ipps._tcp.default.service.arpa.',
3555                    'address': '2001::2',
3556                    'txt_data': 'a=01, b=23dd',
3557                    'srv_ttl': 7100,
3558                    'txt_ttl': 7100,
3559                    'aaaa_ttl': 7100,
3560                }
3561            }
3562        """
3563        cmd = f'dns browse {service_name}'
3564        if server is not None:
3565            cmd += f' {server} {port}'
3566
3567        self.send_command(cmd)
3568        self.simulator.go(10)
3569        output = self._expect_command_output()
3570
3571        # Example output:
3572        # DNS browse response for _ipps._tcp.default.service.arpa.
3573        # ins2
3574        #     Port:22222, Priority:2, Weight:2, TTL:7175
3575        #     Host:host2.default.service.arpa.
3576        #     HostAddress:fd00:db8:0:0:3205:28dd:5b87:6a63 TTL:7175
3577        #     TXT:[a=00, b=11cf] TTL:7175
3578        # ins1
3579        #     Port:11111, Priority:1, Weight:1, TTL:7170
3580        #     Host:host1.default.service.arpa.
3581        #     HostAddress:fd00:db8:0:0:39f4:d9:eb4f:778 TTL:7170
3582        #     TXT:[a=01, b=23dd] TTL:7170
3583        # Done
3584
3585        result = {}
3586        index = 1  # skip first line
3587        while index < len(output):
3588            ins = output[index].strip()
3589            result[ins] = self._parse_dns_service_info(output[index + 1:index + 6])
3590            index = index + (5 if result[ins] else 1)
3591        return result
3592
3593    def set_mliid(self, mliid: str):
3594        cmd = f'mliid {mliid}'
3595        self.send_command(cmd)
3596        self._expect_command_output()
3597
3598    def history_netinfo(self, num_entries=0):
3599        """
3600        Get the `netinfo` history list, parse each entry and return
3601        a list of dictionary (string key and string value) entries.
3602
3603        Example of return value:
3604        [
3605            {
3606                'age': '00:00:00.000 ago',
3607                'role': 'disabled',
3608                'mode': 'rdn',
3609                'rloc16': '0x7400',
3610                'partition-id': '1318093703'
3611            },
3612            {
3613                'age': '00:00:02.588 ago',
3614                'role': 'leader',
3615                'mode': 'rdn',
3616                'rloc16': '0x7400',
3617                'partition-id': '1318093703'
3618            }
3619        ]
3620        """
3621        cmd = f'history netinfo list {num_entries}'
3622        self.send_command(cmd)
3623        output = self._expect_command_output()
3624        netinfos = []
3625        for entry in output:
3626            netinfo = {}
3627            age, info = entry.split(' -> ')
3628            netinfo['age'] = age
3629            for item in info.split(' '):
3630                k, v = item.split(':')
3631                netinfo[k] = v
3632            netinfos.append(netinfo)
3633        return netinfos
3634
3635    def history_rx(self, num_entries=0):
3636        """
3637        Get the IPv6 RX history list, parse each entry and return
3638        a list of dictionary (string key and string value) entries.
3639
3640        Example of return value:
3641        [
3642            {
3643                'age': '00:00:01.999',
3644                'type': 'ICMP6(EchoReqst)',
3645                'len': '16',
3646                'sec': 'yes',
3647                'prio': 'norm',
3648                'rss': '-20',
3649                'from': '0xac00',
3650                'radio': '15.4',
3651                'src': '[fd00:db8:0:0:2cfa:fd61:58a9:f0aa]:0',
3652                'dst': '[fd00:db8:0:0:ed7e:2d04:e543:eba5]:0',
3653            }
3654        ]
3655        """
3656        cmd = f'history rx list {num_entries}'
3657        self.send_command(cmd)
3658        return self._parse_history_rx_tx_ouput(self._expect_command_output())
3659
3660    def history_tx(self, num_entries=0):
3661        """
3662        Get the IPv6 TX history list, parse each entry and return
3663        a list of dictionary (string key and string value) entries.
3664
3665        Example of return value:
3666        [
3667            {
3668                'age': '00:00:01.999',
3669                'type': 'ICMP6(EchoReply)',
3670                'len': '16',
3671                'sec': 'yes',
3672                'prio': 'norm',
3673                'to': '0xac00',
3674                'tx-success': 'yes',
3675                'radio': '15.4',
3676                'src': '[fd00:db8:0:0:ed7e:2d04:e543:eba5]:0',
3677                'dst': '[fd00:db8:0:0:2cfa:fd61:58a9:f0aa]:0',
3678
3679            }
3680        ]
3681        """
3682        cmd = f'history tx list {num_entries}'
3683        self.send_command(cmd)
3684        return self._parse_history_rx_tx_ouput(self._expect_command_output())
3685
3686    def _parse_history_rx_tx_ouput(self, lines):
3687        rxtx_list = []
3688        for line in lines:
3689            if line.strip().startswith('type:'):
3690                for item in line.strip().split(' '):
3691                    k, v = item.split(':')
3692                    entry[k] = v
3693            elif line.strip().startswith('src:'):
3694                entry['src'] = line[4:]
3695            elif line.strip().startswith('dst:'):
3696                entry['dst'] = line[4:]
3697                rxtx_list.append(entry)
3698            else:
3699                entry = {}
3700                entry['age'] = line
3701
3702        return rxtx_list
3703
3704    def set_router_id_range(self, min_router_id: int, max_router_id: int):
3705        cmd = f'routeridrange {min_router_id} {max_router_id}'
3706        self.send_command(cmd)
3707        self._expect_command_output()
3708
3709    def get_router_id_range(self):
3710        cmd = 'routeridrange'
3711        self.send_command(cmd)
3712        line = self._expect_command_output()[0]
3713        return [int(item) for item in line.split()]
3714
3715    def get_channel_monitor_info(self) -> Dict:
3716        """
3717        Returns:
3718            Dict of channel monitor info, e.g.
3719                {'enabled': '1',
3720                 'interval': '41000',
3721                 'threshold': '-75',
3722                 'window': '960',
3723                 'count': '985',
3724                 'occupancies': {
3725                    '11': '0.00%',
3726                    '12': '3.50%',
3727                    '13': '9.89%',
3728                    '14': '15.36%',
3729                    '15': '20.02%',
3730                    '16': '21.95%',
3731                    '17': '32.71%',
3732                    '18': '35.76%',
3733                    '19': '37.97%',
3734                    '20': '43.68%',
3735                    '21': '48.95%',
3736                    '22': '54.05%',
3737                    '23': '58.65%',
3738                    '24': '68.26%',
3739                    '25': '66.73%',
3740                    '26': '73.12%'
3741                    }
3742                }
3743        """
3744        config = {}
3745        self.send_command('channel monitor')
3746
3747        for line in self._expect_results(r'\S+'):
3748            if re.match(r'.*:\s.*', line):
3749                key, val = line.split(':')
3750                config.update({key: val.strip()})
3751            elif re.match(r'.*:', line):  # occupancy
3752                occ_key, val = line.split(':')
3753                val = {}
3754                config.update({occ_key: val})
3755            elif 'busy' in line:
3756                # channel occupancies
3757                key = line.split()[1]
3758                val = line.split()[3]
3759                config[occ_key].update({key: val})
3760        return config
3761
3762    def set_channel_manager_auto_enable(self, enable: bool):
3763        self.send_command(f'channel manager auto {int(enable)}')
3764        self._expect_done()
3765
3766    def set_channel_manager_autocsl_enable(self, enable: bool):
3767        self.send_command(f'channel manager autocsl {int(enable)}')
3768        self._expect_done()
3769
3770    def set_channel_manager_supported(self, channel_mask: int):
3771        self.send_command(f'channel manager supported {int(channel_mask)}')
3772        self._expect_done()
3773
3774    def set_channel_manager_favored(self, channel_mask: int):
3775        self.send_command(f'channel manager favored {int(channel_mask)}')
3776        self._expect_done()
3777
3778    def set_channel_manager_interval(self, interval: int):
3779        self.send_command(f'channel manager interval {interval}')
3780        self._expect_done()
3781
3782    def set_channel_manager_cca_threshold(self, hex_value: str):
3783        self.send_command(f'channel manager threshold {hex_value}')
3784        self._expect_done()
3785
3786    def get_channel_manager_config(self):
3787        self.send_command('channel manager')
3788        return self._expect_key_value_pairs(r'\S+')
3789
3790
3791class Node(NodeImpl, OtCli):
3792    pass
3793
3794
3795class LinuxHost():
3796    PING_RESPONSE_PATTERN = re.compile(r'\d+ bytes from .*:.*')
3797    ETH_DEV = config.BACKBONE_IFNAME
3798
3799    def enable_ether(self):
3800        """Enable the ethernet interface.
3801        """
3802
3803        self.bash(f'ip link set {self.ETH_DEV} up')
3804
3805    def disable_ether(self):
3806        """Disable the ethernet interface.
3807        """
3808
3809        self.bash(f'ip link set {self.ETH_DEV} down')
3810
3811    def get_ether_addrs(self, ipv4=False, ipv6=True):
3812        output = self.bash(f'ip addr list dev {self.ETH_DEV}')
3813
3814        addrs = []
3815        for line in output:
3816            # line examples:
3817            # "inet6 fe80::42:c0ff:fea8:903/64 scope link"
3818            # "inet 192.168.9.1/24 brd 192.168.9.255 scope global eth0"
3819            line = line.strip().split()
3820
3821            if not line or not line[0].startswith('inet'):
3822                continue
3823            if line[0] == 'inet' and not ipv4:
3824                continue
3825            if line[0] == 'inet6' and not ipv6:
3826                continue
3827
3828            addr = line[1]
3829            if '/' in addr:
3830                addr = addr.split('/')[0]
3831            addrs.append(addr)
3832
3833        logging.debug('%s: get_ether_addrs: %r', self, addrs)
3834        return addrs
3835
3836    def get_ether_mac(self):
3837        output = self.bash(f'ip addr list dev {self.ETH_DEV}')
3838        for line in output:
3839            # link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
3840            line = line.strip().split()
3841            if line and line[0] == 'link/ether':
3842                return line[1]
3843
3844        assert False, output
3845
3846    def add_ipmaddr_ether(self, ip: str):
3847        cmd = f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/mcast6.py {self.ETH_DEV} {ip} &'
3848        self.bash(cmd)
3849
3850    def ping_ether(self, ipaddr, num_responses=1, size=None, timeout=5, ttl=None, interface='eth0') -> int:
3851
3852        cmd = f'ping -6 {ipaddr} -I {interface} -c {num_responses} -W {timeout}'
3853        if size is not None:
3854            cmd += f' -s {size}'
3855
3856        if ttl is not None:
3857            cmd += f' -t {ttl}'
3858
3859        resp_count = 0
3860
3861        try:
3862            for line in self.bash(cmd):
3863                if self.PING_RESPONSE_PATTERN.match(line):
3864                    resp_count += 1
3865        except subprocess.CalledProcessError:
3866            pass
3867
3868        return resp_count
3869
3870    def get_ip6_address(self, address_type: config.ADDRESS_TYPE):
3871        """Get specific type of IPv6 address configured on thread device.
3872
3873        Args:
3874            address_type: the config.ADDRESS_TYPE type of IPv6 address.
3875
3876        Returns:
3877            IPv6 address string.
3878        """
3879        if address_type == config.ADDRESS_TYPE.BACKBONE_GUA:
3880            return self._getBackboneGua()
3881        elif address_type == config.ADDRESS_TYPE.BACKBONE_LINK_LOCAL:
3882            return self._getInfraLinkLocalAddress()
3883        elif address_type == config.ADDRESS_TYPE.ONLINK_ULA:
3884            return self._getInfraUla()
3885        elif address_type == config.ADDRESS_TYPE.ONLINK_GUA:
3886            return self._getInfraGua()
3887        else:
3888            raise ValueError(f'unsupported address type: {address_type}')
3889
3890    def _getBackboneGua(self) -> Optional[str]:
3891        for addr in self.get_ether_addrs():
3892            if re.match(config.BACKBONE_PREFIX_REGEX_PATTERN, addr, re.I):
3893                return addr
3894
3895        return None
3896
3897    def _getInfraUla(self) -> Optional[str]:
3898        """ Returns the ULA addresses autoconfigured on the infra link.
3899        """
3900        addrs = []
3901        for addr in self.get_ether_addrs():
3902            if re.match(config.ONLINK_PREFIX_REGEX_PATTERN, addr, re.I):
3903                addrs.append(addr)
3904
3905        return addrs
3906
3907    def _getInfraGua(self) -> Optional[str]:
3908        """ Returns the GUA addresses autoconfigured on the infra link.
3909        """
3910
3911        gua_prefix = config.ONLINK_GUA_PREFIX.split('::/')[0]
3912        return [addr for addr in self.get_ether_addrs() if addr.startswith(gua_prefix)]
3913
3914    def _getInfraLinkLocalAddress(self) -> Optional[str]:
3915        """ Returns the link-local address autoconfigured on the infra link, which is started with "fe80".
3916        """
3917        for addr in self.get_ether_addrs():
3918            if re.match(config.LINK_LOCAL_REGEX_PATTERN, addr, re.I):
3919                return addr
3920
3921        return None
3922
3923    def ping(self, *args, **kwargs):
3924        backbone = kwargs.pop('backbone', False)
3925        if backbone:
3926            return self.ping_ether(*args, **kwargs)
3927        else:
3928            return super().ping(*args, **kwargs)
3929
3930    def udp_send_host(self, ipaddr, port, data, hop_limit=None):
3931        if hop_limit is None:
3932            if ipaddress.ip_address(ipaddr).is_multicast:
3933                hop_limit = 10
3934            else:
3935                hop_limit = 64
3936        cmd = f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/udp_send_host.py {ipaddr} {port} "{data}" {hop_limit}'
3937        self.bash(cmd)
3938
3939    def add_ipmaddr(self, *args, **kwargs):
3940        backbone = kwargs.pop('backbone', False)
3941        if backbone:
3942            return self.add_ipmaddr_ether(*args, **kwargs)
3943        else:
3944            return super().add_ipmaddr(*args, **kwargs)
3945
3946    def ip_neighbors_flush(self):
3947        # clear neigh cache on linux
3948        self.bash(f'ip -6 neigh list dev {self.ETH_DEV}')
3949        self.bash(f'ip -6 neigh flush nud all nud failed nud noarp dev {self.ETH_DEV}')
3950        self.bash('ip -6 neigh list nud all dev %s | cut -d " " -f1 | sudo xargs -I{} ip -6 neigh delete {} dev %s' %
3951                  (self.ETH_DEV, self.ETH_DEV))
3952        self.bash(f'ip -6 neigh list dev {self.ETH_DEV}')
3953
3954    def publish_mdns_service(self, instance_name, service_type, port, host_name, txt):
3955        """Publish an mDNS service on the Ethernet.
3956
3957        :param instance_name: the service instance name.
3958        :param service_type: the service type in format of '<service_type>.<protocol>'.
3959        :param port: the port the service is at.
3960        :param host_name: the host name this service points to. The domain
3961                          should not be included.
3962        :param txt: a dictionary containing the key-value pairs of the TXT record.
3963        """
3964        txt_string = ' '.join([f'{key}={value}' for key, value in txt.items()])
3965        self.bash(f'avahi-publish -s {instance_name}  {service_type} {port} -H {host_name}.local {txt_string} &')
3966
3967    def publish_mdns_host(self, hostname, addresses):
3968        """Publish an mDNS host on the Ethernet
3969
3970        :param host_name: the host name this service points to. The domain
3971                          should not be included.
3972        :param addresses: a list of strings representing the addresses to
3973                          be registered with the host.
3974        """
3975        for address in addresses:
3976            self.bash(f'avahi-publish -a {hostname}.local {address} &')
3977
3978    def browse_mdns_services(self, name, timeout=2):
3979        """ Browse mDNS services on the ethernet.
3980
3981        :param name: the service type name in format of '<service-name>.<protocol>'.
3982        :param timeout: timeout value in seconds before returning.
3983        :return: A list of service instance names.
3984        """
3985
3986        self.bash(f'dns-sd -Z {name} local. > /tmp/{name} 2>&1 &')
3987        time.sleep(timeout)
3988        self.bash('pkill dns-sd')
3989
3990        instances = []
3991        for line in self.bash(f'cat /tmp/{name}', encoding='raw_unicode_escape'):
3992            elements = line.split()
3993            if len(elements) >= 3 and elements[0] == name and elements[1] == 'PTR':
3994                instances.append(elements[2][:-len('.' + name)])
3995        return instances
3996
3997    def discover_mdns_service(self, instance, name, host_name, timeout=2):
3998        """ Discover/resolve the mDNS service on ethernet.
3999
4000        :param instance: the service instance name.
4001        :param name: the service name in format of '<service-name>.<protocol>'.
4002        :param host_name: the host name this service points to. The domain
4003                          should not be included.
4004        :param timeout: timeout value in seconds before returning.
4005        :return: a dict of service properties or None.
4006
4007        The return value is a dict with the same key/values of srp_server_get_service
4008        except that we don't have a `deleted` field here.
4009        """
4010        host_name_file = self.bash('mktemp')[0].strip()
4011        service_data_file = self.bash('mktemp')[0].strip()
4012
4013        self.bash(f'dns-sd -Z {name} local. > {service_data_file} 2>&1 &')
4014        time.sleep(timeout)
4015
4016        full_service_name = f'{instance}.{name}'
4017        # When hostname is unspecified, extract hostname from browse result
4018        if host_name is None:
4019            for line in self.bash(f'cat {service_data_file}', encoding='raw_unicode_escape'):
4020                elements = line.split()
4021                if len(elements) >= 6 and elements[0] == full_service_name and elements[1] == 'SRV':
4022                    host_name = elements[5].split('.')[0]
4023                    break
4024
4025        assert (host_name is not None)
4026        self.bash(f'dns-sd -G v6 {host_name}.local. > {host_name_file} 2>&1 &')
4027        time.sleep(timeout)
4028
4029        self.bash('pkill dns-sd')
4030        addresses = []
4031        service = {}
4032
4033        logging.debug(self.bash(f'cat {host_name_file}', encoding='raw_unicode_escape'))
4034        logging.debug(self.bash(f'cat {service_data_file}', encoding='raw_unicode_escape'))
4035
4036        # example output in the host file:
4037        # Timestamp     A/R Flags if Hostname                               Address                                     TTL
4038        # 9:38:09.274  Add     23 48 my-host.local.                         2001:0000:0000:0000:0000:0000:0000:0002%<0>  120
4039        #
4040        for line in self.bash(f'cat {host_name_file}', encoding='raw_unicode_escape'):
4041            elements = line.split()
4042            fullname = f'{host_name}.local.'
4043            if 'No Such Record' in line:
4044                continue
4045            if fullname not in elements:
4046                continue
4047            if 'Add' not in elements:
4048                continue
4049            addresses.append(elements[elements.index(fullname) + 1].split('%')[0])
4050
4051        logging.debug(f'addresses of {host_name}: {addresses}')
4052
4053        # example output of in the service file:
4054        # _ipps._tcp                                      PTR     my-service._ipps._tcp
4055        # my-service._ipps._tcp                           SRV     0 0 12345 my-host.local. ; Replace with unicast FQDN of target host
4056        # my-service._ipps._tcp                           TXT     ""
4057        #
4058        is_txt = False
4059        txt = ''
4060        for line in self.bash(f'cat {service_data_file}', encoding='raw_unicode_escape'):
4061            elements = line.split()
4062            if len(elements) >= 2 and elements[0] == full_service_name and elements[1] == 'TXT':
4063                is_txt = True
4064            if is_txt:
4065                txt += line.strip()
4066                if line.strip().endswith('"'):
4067                    is_txt = False
4068                    txt_dict = self.__parse_dns_sd_txt(txt)
4069                    logging.info(f'txt = {txt_dict}')
4070                    service['txt'] = txt_dict
4071
4072            if not elements or elements[0] != full_service_name:
4073                continue
4074            if elements[1] == 'SRV':
4075                service['fullname'] = elements[0]
4076                service['instance'] = instance
4077                service['name'] = name
4078                service['priority'] = int(elements[2])
4079                service['weight'] = int(elements[3])
4080                service['port'] = int(elements[4])
4081                service['host_fullname'] = elements[5]
4082                assert (service['host_fullname'] == f'{host_name}.local.')
4083                service['host'] = host_name
4084                service['addresses'] = addresses
4085        return service or None
4086
4087    def start_radvd_service(self, prefix, slaac):
4088        self.bash("""cat >/etc/radvd.conf <<EOF
4089interface eth0
4090{
4091    AdvSendAdvert on;
4092
4093    AdvReachableTime 200;
4094    AdvRetransTimer 200;
4095    AdvDefaultLifetime 1800;
4096    MinRtrAdvInterval 1200;
4097    MaxRtrAdvInterval 1800;
4098    AdvDefaultPreference low;
4099
4100    prefix %s
4101    {
4102        AdvOnLink on;
4103        AdvAutonomous %s;
4104        AdvRouterAddr off;
4105        AdvPreferredLifetime 1800;
4106        AdvValidLifetime 1800;
4107    };
4108};
4109EOF
4110""" % (prefix, 'on' if slaac else 'off'))
4111        self.bash('service radvd start')
4112        self.bash('service radvd status')  # Make sure radvd service is running
4113
4114    def start_pd_radvd_service(self, prefix):
4115        self.bash("""cat >/etc/radvd.conf <<EOF
4116interface wpan0
4117{
4118    AdvSendAdvert on;
4119
4120    AdvReachableTime 20;
4121    AdvRetransTimer 20;
4122    AdvDefaultLifetime 180;
4123    MinRtrAdvInterval 120;
4124    MaxRtrAdvInterval 180;
4125    AdvDefaultPreference low;
4126
4127    prefix %s
4128    {
4129        AdvOnLink on;
4130        AdvAutonomous on;
4131        AdvRouterAddr off;
4132        AdvPreferredLifetime 180;
4133        AdvValidLifetime 180;
4134    };
4135};
4136EOF
4137""" % (prefix,))
4138        self.bash('service radvd start')
4139        self.bash('service radvd status')  # Make sure radvd service is running
4140
4141    def stop_radvd_service(self):
4142        self.bash('service radvd stop')
4143
4144    def kill_radvd_service(self):
4145        self.bash('pkill radvd')
4146
4147    def __parse_dns_sd_txt(self, line: str):
4148        # Example TXT entry:
4149        # "xp=\\000\\013\\184\\000\\000\\000\\000\\000"
4150        txt = {}
4151        for entry in re.findall(r'"((?:[^\\]|\\.)*?)"', line):
4152            if '=' not in entry:
4153                continue
4154
4155            k, v = entry.split('=', 1)
4156            txt[k] = v
4157
4158        return txt
4159
4160
4161class OtbrNode(LinuxHost, NodeImpl, OtbrDocker):
4162    TUN_DEV = config.THREAD_IFNAME
4163    is_otbr = True
4164    is_bbr = True  # OTBR is also BBR
4165    node_type = 'otbr-docker'
4166
4167    def __repr__(self):
4168        return f'Otbr<{self.nodeid}>'
4169
4170    def start(self):
4171        self._setup_sysctl()
4172        self.set_log_level(5)
4173        super().start()
4174
4175    def add_ipaddr(self, addr):
4176        cmd = f'ip -6 addr add {addr}/64 dev {self.TUN_DEV}'
4177        self.bash(cmd)
4178
4179    def add_ipmaddr_tun(self, ip: str):
4180        cmd = f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/mcast6.py {self.TUN_DEV} {ip} &'
4181        self.bash(cmd)
4182
4183    def get_ip6_address(self, address_type: config.ADDRESS_TYPE):
4184        try:
4185            return super(OtbrNode, self).get_ip6_address(address_type)
4186        except Exception as e:
4187            return super(LinuxHost, self).get_ip6_address(address_type)
4188
4189
4190class HostNode(LinuxHost, OtbrDocker):
4191    is_host = True
4192
4193    def __init__(self, nodeid, name=None, **kwargs):
4194        self.nodeid = nodeid
4195        self.name = name or ('Host%d' % nodeid)
4196        super().__init__(nodeid, **kwargs)
4197        self.bash('service otbr-agent stop')
4198
4199    def start(self, start_radvd=True, prefix=config.DOMAIN_PREFIX, slaac=False):
4200        self._setup_sysctl()
4201        if start_radvd:
4202            self.start_radvd_service(prefix, slaac)
4203        else:
4204            self.stop_radvd_service()
4205
4206    def stop(self):
4207        self.stop_radvd_service()
4208
4209    def get_addrs(self) -> List[str]:
4210        return self.get_ether_addrs()
4211
4212    def __repr__(self):
4213        return f'Host<{self.nodeid}>'
4214
4215    def get_matched_ula_addresses(self, prefix):
4216        """Get the IPv6 addresses that matches given prefix.
4217        """
4218
4219        addrs = []
4220        for addr in self.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA):
4221            if IPv6Address(addr) in IPv6Network(prefix):
4222                addrs.append(addr)
4223
4224        return addrs
4225
4226
4227if __name__ == '__main__':
4228    unittest.main()
4229