diff --git a/.github/workflows/otbr.yml b/.github/workflows/otbr.yml index 4a034135d..173035f41 100644 --- a/.github/workflows/otbr.yml +++ b/.github/workflows/otbr.yml @@ -138,24 +138,33 @@ jobs: - otbr_mdns: "mDNSResponder" otbr_trel: 0 cert_scripts: ./tests/scripts/thread-cert/border_router/*.py + packet_verification: 1 description: "" - otbr_mdns: "mDNSResponder" otbr_trel: 1 cert_scripts: ./tests/scripts/thread-cert/border_router/*.py + packet_verification: 2 description: "" - otbr_mdns: "mDNSResponder" otbr_trel: 0 cert_scripts: ./tests/scripts/thread-cert/border_router/MATN/*.py + packet_verification: 1 description: "MATN" - otbr_mdns: "avahi" otbr_trel: 0 cert_scripts: ./tests/scripts/thread-cert/border_router/*.py + packet_verification: 1 + description: "" + - otbr_mdns: "avahi" + otbr_trel: 1 + cert_scripts: ./tests/scripts/thread-cert/border_router/*.py + packet_verification: 2 description: "" name: BR ${{ matrix.description }} (${{ matrix.otbr_mdns }}, TREL=${{matrix.otbr_trel}}) env: REFERENCE_DEVICE: 1 VIRTUAL_TIME: 0 - PACKET_VERIFICATION: 1 + PACKET_VERIFICATION: ${{ matrix.packet_verification }} THREAD_VERSION: 1.2 INTER_OP: 1 COVERAGE: 1 diff --git a/script/test b/script/test index 5aacf0512..ef2ba7254 100755 --- a/script/test +++ b/script/test @@ -290,9 +290,9 @@ do_build_otbr_docker() ) if [[ ${TREL} == 1 ]]; then - otbr_options+=("-DOT_TREL=ON") + otbr_options+=("-DOTBR_TREL=ON") else - otbr_options+=("-DOT_TREL=OFF") + otbr_options+=("-DOTBR_TREL=OFF") fi local otbr_docker_image=${OTBR_DOCKER_IMAGE:-otbr-ot12-backbone-ci} diff --git a/src/posix/platform/trel.cpp b/src/posix/platform/trel.cpp index 03225ca8c..91758755e 100644 --- a/src/posix/platform/trel.cpp +++ b/src/posix/platform/trel.cpp @@ -68,6 +68,7 @@ static TxPacket sTxPacketPool[TREL_PACKET_POOL_SIZE]; static TxPacket *sFreeTxPacketHead; // A singly linked list of free/available `TxPacket` from pool. static TxPacket *sTxPacketQueueTail; // A circular linked list for queued tx packets. +static char sInterfaceName[IFNAMSIZ + 1]; static bool sInitialized = false; static bool sEnabled = false; static int sSocket = -1; @@ -318,6 +319,14 @@ exit: // behavior. They need to be overridden during project/platform // integration. +OT_TOOL_WEAK void trelDnssdInitialize(const char *aTrelNetif) +{ + // This function initialize the TREL DNS-SD module on the given + // TREL Network Interface. + + OT_UNUSED_VARIABLE(aTrelNetif); +} + OT_TOOL_WEAK void trelDnssdStartBrowse(void) { // This function initiates an ongoing DNS-SD browse on the service @@ -490,11 +499,18 @@ exit: void platformTrelInit(const char *aTrelUrl) { - OT_UNUSED_VARIABLE(aTrelUrl); + otLogDebgPlat("[trel] platformTrelInit(aTrelUrl:\"%s\")", aTrelUrl != nullptr ? aTrelUrl : ""); assert(!sInitialized); - otLogDebgPlat("[trel] platformTrelInit(aTrelUrl:\"%s\")", aTrelUrl != nullptr ? aTrelUrl : ""); + if (aTrelUrl != nullptr) + { + ot::Posix::RadioUrl url(aTrelUrl); + strncpy(sInterfaceName, url.GetPath(), sizeof(sInterfaceName) - 1); + sInterfaceName[sizeof(sInterfaceName) - 1] = '\0'; + } + + trelDnssdInitialize(sInterfaceName); InitPacketQueue(); sInitialized = true; @@ -505,7 +521,8 @@ void platformTrelDeinit(void) VerifyOrExit(sInitialized); otPlatTrelDisable(nullptr); - sInitialized = false; + sInterfaceName[0] = '\0'; + sInitialized = false; otLogDebgPlat("[trel] platformTrelDeinit()"); exit: diff --git a/tests/scripts/thread-cert/border_router/test_trel_connectivity.py b/tests/scripts/thread-cert/border_router/test_trel_connectivity.py new file mode 100644 index 000000000..9d9f31573 --- /dev/null +++ b/tests/scripts/thread-cert/border_router/test_trel_connectivity.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +import unittest + +import thread_cert +# Test description: +# This test verifies TREL connectivity. +# +# Topology: +# ----------------(eth)-------------------------- +# | | +# FED1 --- BR1 (Leader) ----- BR2 --- ROUTER2 +# /\ +# / \ +# MED1 SED1 +# +from config import PACKET_VERIFICATION_TREL +from pktverify.packet_filter import PacketFilter +from pktverify.packet_verifier import PacketVerifier + +BR1 = 1 +FED1 = 2 +MED1 = 3 +SED1 = 4 +BR2 = 5 +ROUTER2 = 6 + + +class TestTrelConnectivity(thread_cert.TestCase): + USE_MESSAGE_FACTORY = False + PACKET_VERIFICATION = PACKET_VERIFICATION_TREL + + TOPOLOGY = { + BR1: { + 'name': 'BR1', + 'allowlist': [BR2, FED1, MED1, SED1], + 'is_otbr': True, + 'version': '1.2', + }, + FED1: { + 'name': 'ROUTER1', + 'allowlist': [BR1], + 'version': '1.2', + 'router_eligible': False, + }, + MED1: { + 'name': 'MED1', + 'allowlist': [BR1], + 'version': '1.2', + 'mode': 'rn', + }, + SED1: { + 'name': 'SED1', + 'allowlist': [BR1], + 'version': '1.2', + 'mode': 'n', + }, + BR2: { + 'name': 'BR2', + 'allowlist': [BR1, ROUTER2], + 'is_otbr': True, + 'version': '1.2', + }, + ROUTER2: { + 'name': 'ROUTER2', + 'allowlist': [BR2], + 'version': '1.2', + }, + } + + def test(self): + br1 = self.nodes[BR1] + fed1 = self.nodes[FED1] + med1 = self.nodes[MED1] + sed1 = self.nodes[SED1] + br2 = self.nodes[BR2] + router2 = self.nodes[ROUTER2] + + if br1.get_trel_state() is None: + self.skipTest("TREL is not enabled") + + br1.start() + self.wait_node_state(br1, 'leader', 10) + + fed1.start() + self.wait_node_state(fed1, 'child', 10) + + med1.start() + self.wait_node_state(med1, 'child', 10) + + sed1.start() + self.wait_node_state(sed1, 'child', 10) + + br2.start() + self.wait_node_state(br2, 'router', 10) + + router2.start() + self.wait_node_state(router2, 'router', 10) + + # Allow the network to stabilize + self.simulator.go(10) + + self.collect_ipaddrs() + self.collect_rloc16s() + + router2_mleid = router2.get_mleid() + self.assertTrue(br1.ping(router2_mleid)) + self.assertTrue(fed1.ping(router2_mleid)) + self.assertTrue(med1.ping(router2_mleid)) + self.assertTrue(sed1.ping(router2_mleid)) + + def verify(self, pv: PacketVerifier): + pkts: PacketFilter = pv.pkts + BR1_RLOC16 = pv.vars['BR1_RLOC16'] + BR2_RLOC16 = pv.vars['BR2_RLOC16'] + + print('BR1_RLOC16:', hex(BR1_RLOC16)) + print('BR2_RLOC16:', hex(BR2_RLOC16)) + + # Make sure BR1 and BR2 always use TREL for transmitting ping request and reply + pkts.filter_wpan_src16_dst16(BR1_RLOC16, BR2_RLOC16).filter_ping_request().must_not_next() + pkts.filter_wpan_src16_dst16(BR1_RLOC16, BR2_RLOC16).filter_ping_reply().must_not_next() + + pkts.filter_wpan_src16_dst16(BR2_RLOC16, BR1_RLOC16).filter_ping_request().must_not_next() + pkts.filter_wpan_src16_dst16(BR2_RLOC16, BR1_RLOC16).filter_ping_reply().must_not_next() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/scripts/thread-cert/config.py b/tests/scripts/thread-cert/config.py index 18c8c888e..22958f6a1 100755 --- a/tests/scripts/thread-cert/config.py +++ b/tests/scripts/thread-cert/config.py @@ -147,6 +147,10 @@ LEADER_NOTIFY_SED_BY_CHILD_UPDATE_REQUEST = True THREAD_VERSION_1_1 = 2 THREAD_VERSION_1_2 = 3 +PACKET_VERIFICATION_NONE = 0 +PACKET_VERIFICATION_DEFAULT = 1 +PACKET_VERIFICATION_TREL = 2 + def create_default_network_data_prefix_sub_tlvs_factories(): return { diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index 98194fccd..3191a5027 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -125,6 +125,8 @@ class OtbrDocker: config.OTBR_DOCKER_IMAGE, '-B', config.BACKBONE_IFNAME, + '--trel-url', + f'trel://{config.BACKBONE_IFNAME}', ], stdin=subprocess.DEVNULL, stdout=sys.stdout, @@ -1145,6 +1147,21 @@ class NodeImpl: self.send_command(cmd) return int(self._expect_result('\d+')) + # + # TREL utilities + # + + def get_trel_state(self) -> Union[None, bool]: + states = [r'Disabled', r'Enabled'] + self.send_command('trel') + try: + return self._expect_result(states) == 'Enabled' + except Exception as ex: + if 'InvalidCommand' in str(ex): + return None + + raise + def _encode_txt_entry(self, entry): """Encodes the TXT entry to the DNS-SD TXT record format as a HEX string. diff --git a/tests/scripts/thread-cert/thread_cert.py b/tests/scripts/thread-cert/thread_cert.py index 6664387ca..341fa988b 100755 --- a/tests/scripts/thread-cert/thread_cert.py +++ b/tests/scripts/thread-cert/thread_cert.py @@ -38,7 +38,7 @@ import sys import time import traceback import unittest -from typing import Optional, Callable +from typing import Optional, Callable, Union, Any import config import debug @@ -101,6 +101,7 @@ class TestCase(NcpSupportMixin, unittest.TestCase): TOPOLOGY = None CASE_WIRESHARK_PREFS = None SUPPORT_THREAD_1_1 = True + PACKET_VERIFICATION = config.PACKET_VERIFICATION_DEFAULT def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -108,9 +109,16 @@ class TestCase(NcpSupportMixin, unittest.TestCase): logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') self._start_time = None - self._do_packet_verification = PACKET_VERIFICATION and hasattr(self, 'verify') + self._do_packet_verification = PACKET_VERIFICATION and hasattr(self, 'verify') \ + and self.PACKET_VERIFICATION == PACKET_VERIFICATION + + def skipTest(self, reason: Any) -> None: + self._testSkipped = True + super(TestCase, self).skipTest(reason) def setUp(self): + self._testSkipped = False + if ENV_THREAD_VERSION == '1.1' and not self.SUPPORT_THREAD_1_1: self.skipTest('Thread 1.1 not supported.') @@ -290,7 +298,8 @@ class TestCase(NcpSupportMixin, unittest.TestCase): self._test_info['pcap'] = pcap_filename test_info_path = self._output_test_info() - self._verify_packets(test_info_path) + if not self._testSkipped: + self._verify_packets(test_info_path) def flush_all(self): """Flush away all captured messages of all nodes. @@ -558,8 +567,9 @@ class TestCase(NcpSupportMixin, unittest.TestCase): if timeout <= 0: raise RuntimeError(f'wait failed after {timeout} seconds') - def wait_node_state(self, nodeid: int, state: str, timeout: int): - self.wait_until(lambda: self.nodes[nodeid].get_state() == state, timeout) + def wait_node_state(self, node: Union[int, Node], state: str, timeout: int): + node = self.nodes[node] if isinstance(node, int) else node + self.wait_until(lambda: node.get_state() == state, timeout) def wait_route_established(self, node1: int, node2: int, timeout=10): node2_addr = self.nodes[node2].get_ip6_address(config.ADDRESS_TYPE.RLOC)