From 282f8a4a8bc73d42217d59830be3fc7de1901232 Mon Sep 17 00:00:00 2001 From: kangping Date: Wed, 10 Feb 2021 04:18:16 +0800 Subject: [PATCH] [dns-sd] add CI tests for SRP server & Advertising Proxy (#6131) This commit includes basic service registration & advertising function verification. More tests including name conflicts and multiple devices cases will be in future commits. We changed the border_routing test scripts directory to border_router as there will be more tests about Thread border router but not only the routing feature. --- .github/workflows/simulation-1.2.yml | 12 +- script/test | 2 +- src/core/net/srp_server.cpp | 1 + .../thread-cert/backbone/bbr_5_11_01.py | 5 + .../border_router/test_advertising_proxy.py | 189 ++++++++++++++++++ .../test_multi_border_routers.py | 0 .../test_multi_thread_networks.py | 0 .../test_plat_udp_accessiblity.py | 0 .../test_single_border_router.py | 0 tests/scripts/thread-cert/node.py | 39 ++++ tests/scripts/thread-cert/requirements.txt | 1 + 11 files changed, 242 insertions(+), 7 deletions(-) create mode 100755 tests/scripts/thread-cert/border_router/test_advertising_proxy.py rename tests/scripts/thread-cert/{border_routing => border_router}/test_multi_border_routers.py (100%) rename tests/scripts/thread-cert/{border_routing => border_router}/test_multi_thread_networks.py (100%) rename tests/scripts/thread-cert/{border_routing => border_router}/test_plat_udp_accessiblity.py (100%) rename tests/scripts/thread-cert/{border_routing => border_router}/test_single_border_router.py (100%) diff --git a/.github/workflows/simulation-1.2.yml b/.github/workflows/simulation-1.2.yml index 5e2b071b4..a3b76c918 100644 --- a/.github/workflows/simulation-1.2.yml +++ b/.github/workflows/simulation-1.2.yml @@ -303,7 +303,7 @@ jobs: name: cov-thread-1-2-backbone path: tmp/coverage.info - thread-border-routing: + thread-border-router: runs-on: ubuntu-20.04 env: REFERENCE_DEVICE: 1 @@ -337,15 +337,15 @@ jobs: run: | export CI_ENV="$(bash <(curl -s https://codecov.io/env)) -e GITHUB_ACTIONS -e COVERAGE" echo "CI_ENV=${CI_ENV}" - sudo -E ./script/test cert_suite ./tests/scripts/thread-cert/border_routing/*.py || (sudo chmod a+r *.log *.json *.pcap && false) + sudo -E ./script/test cert_suite ./tests/scripts/thread-cert/border_router/*.py || (sudo chmod a+r *.log *.json *.pcap && false) - uses: actions/upload-artifact@v2 with: - name: cov-thread-border-routing-docker + name: cov-thread-border-router-docker path: /tmp/coverage/ - uses: actions/upload-artifact@v2 if: ${{ failure() }} with: - name: thread-border-routing-results + name: thread-border-router-results path: | *.pcap *.json @@ -355,7 +355,7 @@ jobs: ./script/test generate_coverage gcc - uses: actions/upload-artifact@v2 with: - name: cov-thread-border-routing + name: cov-thread-border-router path: tmp/coverage.info upload-coverage: @@ -365,7 +365,7 @@ jobs: - packet-verification-1-1-on-1-2 - expects - thread-1-2-backbone - - thread-border-routing + - thread-border-router runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 diff --git a/script/test b/script/test index 66c9073d6..b5c2aec47 100755 --- a/script/test +++ b/script/test @@ -255,7 +255,7 @@ do_build_otbr_docker() echo "Building OTBR Docker ..." local otdir local otbrdir - local otbr_options="-DOT_SLAAC=ON -DOT_DUA=ON -DOT_MLR=ON -DOT_COVERAGE=ON -DOTBR_REST=OFF -DOTBR_WEB=OFF" + local otbr_options="-DOT_SLAAC=ON -DOT_DUA=ON -DOT_MLR=ON -DOT_COVERAGE=ON -DOTBR_REST=OFF -DOTBR_WEB=OFF -DOTBR_MDNS=mDNSResponder -DOTBR_SRP_ADVERTISING_PROXY=ON" local otbr_docker_image=${OTBR_DOCKER_IMAGE:-otbr-ot12-backbone-ci} # Always enable SRP server for OTBR. diff --git a/src/core/net/srp_server.cpp b/src/core/net/srp_server.cpp index 3244489da..45a7fa76b 100644 --- a/src/core/net/srp_server.cpp +++ b/src/core/net/srp_server.cpp @@ -1254,6 +1254,7 @@ void Server::HandleOutstandingUpdatesTimer(Timer &aTimer) void Server::HandleOutstandingUpdatesTimer(void) { + otLogInfoSrp("[server] outstanding service update timeout"); while (!mOutstandingUpdates.IsEmpty() && mOutstandingUpdates.GetTail()->GetExpireTime() <= TimerMilli::GetNow()) { HandleAdvertisingResult(mOutstandingUpdates.GetTail(), OT_ERROR_RESPONSE_TIMEOUT); diff --git a/tests/scripts/thread-cert/backbone/bbr_5_11_01.py b/tests/scripts/thread-cert/backbone/bbr_5_11_01.py index 57f0c475b..9c0352ab3 100755 --- a/tests/scripts/thread-cert/backbone/bbr_5_11_01.py +++ b/tests/scripts/thread-cert/backbone/bbr_5_11_01.py @@ -91,6 +91,11 @@ class BBR_5_11_01(thread_cert.TestCase): self.simulator.go(5) self.assertEqual('router', self.nodes[ROUTER2].get_state()) + # The OTBR docker enables SRP Server by default, lets explicitly + # disable SRP server to avoid Network Data population. + # TODO: Enhance the test script to tolerate additional Sertivce TLV + # in Network Data. + self.nodes[BR_1].srp_server_set_enabled(False) self.nodes[BR_1].start() self.simulator.go(5) self.assertEqual('router', self.nodes[BR_1].get_state()) diff --git a/tests/scripts/thread-cert/border_router/test_advertising_proxy.py b/tests/scripts/thread-cert/border_router/test_advertising_proxy.py new file mode 100755 index 000000000..6c67db67a --- /dev/null +++ b/tests/scripts/thread-cert/border_router/test_advertising_proxy.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +import ipaddress +import logging +import unittest + +import config +import thread_cert + +# Test description: +# This test verifies the basic functionality of advertising proxy. +# +# Topology: +# ----------------(eth)-------------------- +# | | +# BR (Leader) HOST (mDNS Browser) +# | +# ROUTER +# + +BR = 1 +ROUTER = 2 +HOST = 3 + + +class SingleHostAndService(thread_cert.TestCase): + USE_MESSAGE_FACTORY = False + + TOPOLOGY = { + BR: { + 'name': 'BR_1', + 'allowlist': [ROUTER], + 'is_otbr': True, + 'version': '1.2', + 'router_selection_jitter': 2, + }, + ROUTER: { + 'name': 'Router_1', + 'allowlist': [BR], + 'version': '1.2', + 'router_selection_jitter': 2, + }, + HOST: { + 'name': 'Host', + 'is_host': True + }, + } + + def test(self): + host = self.nodes[HOST] + server = self.nodes[BR] + client = self.nodes[ROUTER] + + host.start(start_radvd=False) + self.simulator.go(5) + + server.srp_server_set_enabled(True) + server.start() + self.simulator.go(5) + self.assertEqual('leader', server.get_state()) + + client.start() + self.simulator.go(5) + self.assertEqual('router', client.get_state()) + + # + # 1. Register a single service. + # + + client.srp_client_set_host_name('my-host') + client.srp_client_set_host_address('2001::1') + client.srp_client_add_service('my-service', '_ipps._tcp', 12345) + client.srp_client_start(server.get_addrs()[0], client.get_srp_server_port()) + self.simulator.go(2) + + self.check_host_and_service(server, client) + + # + # 2. Discover the service by the HOST on the ethernet. This makes sure + # the Advertising Proxy multicasts the same service on ethernet. + # + + self.host_check_mdns_service(host) + + # + # 3. Check if the Advertising Proxy removes the service from ethernet + # when the SRP client removes it. + # + + client.srp_client_remove_host() + self.simulator.go(2) + + self.assertIsNone(host.discover_mdns_service('my-service', '_ipps._tcp', 'my-host')) + + # + # 4. Check if we can discover the mDNS service again when re-registering the + # service from the SRP client. + # + + client.srp_client_set_host_name('my-host') + client.srp_client_set_host_address('2001::1') + client.srp_client_add_service('my-service', '_ipps._tcp', 12345) + self.simulator.go(2) + + self.host_check_mdns_service(host) + + def host_check_mdns_service(self, host): + service = host.discover_mdns_service('my-service', '_ipps._tcp', 'my-host') + self.assertIsNotNone(service) + self.assertEqual(service['instance'], 'my-service') + self.assertEqual(service['name'], '_ipps._tcp') + self.assertEqual(service['port'], 12345) + self.assertEqual(service['priority'], 0) + self.assertEqual(service['weight'], 0) + self.assertEqual(service['host'], 'my-host') + + def check_host_and_service(self, server, client): + """Check that we have properly registered host and service instance. + """ + + client_services = client.srp_client_get_services() + print(client_services) + self.assertEqual(len(client_services), 1) + client_service = client_services[0] + + # Verify that the client possesses correct service resources. + self.assertEqual(client_service['instance'], 'my-service') + self.assertEqual(client_service['name'], '_ipps._tcp') + self.assertEqual(int(client_service['port']), 12345) + self.assertEqual(int(client_service['priority']), 0) + self.assertEqual(int(client_service['weight']), 0) + + # Verify that the client received a SUCCESS response for the server. + self.assertEqual(client_service['state'], 'Registered') + + server_services = server.srp_server_get_services() + print(server_services) + self.assertEqual(len(server_services), 1) + server_service = server_services[0] + + # Verify that the server accepted the SRP registration and stores + # the same service resources. + self.assertEqual(server_service['deleted'], 'false') + self.assertEqual(server_service['instance'], client_service['instance']) + self.assertEqual(server_service['name'], client_service['name']) + self.assertEqual(int(server_service['port']), int(client_service['port'])) + self.assertEqual(int(server_service['priority']), int(client_service['priority'])) + self.assertEqual(int(server_service['weight']), int(client_service['weight'])) + self.assertEqual(server_service['host'], 'my-host') + + server_hosts = server.srp_server_get_hosts() + print(server_hosts) + self.assertEqual(len(server_hosts), 1) + server_host = server_hosts[0] + + self.assertEqual(server_host['deleted'], 'false') + self.assertEqual(server_host['fullname'], server_service['host_fullname']) + self.assertEqual(len(server_host['addresses']), 1) + self.assertEqual(ipaddress.ip_address(server_host['addresses'][0]), ipaddress.ip_address('2001::1')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/scripts/thread-cert/border_routing/test_multi_border_routers.py b/tests/scripts/thread-cert/border_router/test_multi_border_routers.py similarity index 100% rename from tests/scripts/thread-cert/border_routing/test_multi_border_routers.py rename to tests/scripts/thread-cert/border_router/test_multi_border_routers.py diff --git a/tests/scripts/thread-cert/border_routing/test_multi_thread_networks.py b/tests/scripts/thread-cert/border_router/test_multi_thread_networks.py similarity index 100% rename from tests/scripts/thread-cert/border_routing/test_multi_thread_networks.py rename to tests/scripts/thread-cert/border_router/test_multi_thread_networks.py diff --git a/tests/scripts/thread-cert/border_routing/test_plat_udp_accessiblity.py b/tests/scripts/thread-cert/border_router/test_plat_udp_accessiblity.py similarity index 100% rename from tests/scripts/thread-cert/border_routing/test_plat_udp_accessiblity.py rename to tests/scripts/thread-cert/border_router/test_plat_udp_accessiblity.py diff --git a/tests/scripts/thread-cert/border_routing/test_single_border_router.py b/tests/scripts/thread-cert/border_router/test_single_border_router.py similarity index 100% rename from tests/scripts/thread-cert/border_routing/test_single_border_router.py rename to tests/scripts/thread-cert/border_router/test_single_border_router.py diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index dc4f74b1f..1efc3d3af 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -39,6 +39,7 @@ import time import traceback import unittest from typing import Union, Dict, Optional, List +from zeroconf import Zeroconf, ServiceInfo import pexpect import pexpect.popen_spawn @@ -221,6 +222,9 @@ class OtbrDocker: self.bash(f'sysctl net.ipv6.conf.{self.ETH_DEV}.accept_ra=2') self.bash(f'sysctl net.ipv6.conf.{self.ETH_DEV}.accept_ra_rt_info_max_plen=64') + # Zeroconf complains that there is not enough BUFS to subscribe multicast groups. + self.bash('sysctl net.ipv4.igmp_max_memberships=1024') + class OtCli: @@ -2417,6 +2421,41 @@ class LinuxHost(): (self.ETH_DEV, self.ETH_DEV)) self.bash(f'ip -6 neigh list dev {self.ETH_DEV}') + def discover_mdns_service(self, instance, name, host_name, timeout=2): + """ Discover/resolve the mDNS service on ethernet. + + :param instance: the service instance name. + :param name: the service name in format of '.'. + :param host_name: the host name this service points to. The domain + should not be included. + :param timeout: timeout value in seconds before returning. + :return: a dict of service properties or None. + + The return value is a dict with the same key/values of srp_server_get_service + except that we don't have a `deleted` field here. + """ + zeroconf = Zeroconf() + timeout *= 1000 # Zeroconf use timeout in milliseconds. + try: + info = ServiceInfo(type_=f'{name}.local.', name=f'{instance}.{name}.local.', server=f'{host_name}.local.') + while timeout > 0 and not info.parsed_addresses(): + info.request(zeroconf, 500) + timeout -= 500 + if info.parsed_addresses(): + return { + 'fullname': info.name, + 'instance': info.get_name(), + 'name': name, + 'port': info.port, + 'weight': info.weight, + 'priority': info.priority, + 'host_fullname': info.server, + 'host': host_name, + 'addresses': info.parsed_addresses() + } + finally: + zeroconf.close() + class OtbrNode(LinuxHost, NodeImpl, OtbrDocker): is_otbr = True diff --git a/tests/scripts/thread-cert/requirements.txt b/tests/scripts/thread-cert/requirements.txt index 584eae7c7..d23ea1b98 100644 --- a/tests/scripts/thread-cert/requirements.txt +++ b/tests/scripts/thread-cert/requirements.txt @@ -2,3 +2,4 @@ ipaddress pexpect pycryptodome pyshark==0.4.2.11 +zeroconf