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