[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.
This commit is contained in:
kangping
2021-02-09 12:18:16 -08:00
committed by GitHub
parent 3a37d347ee
commit 282f8a4a8b
11 changed files with 242 additions and 7 deletions
+6 -6
View File
@@ -303,7 +303,7 @@ jobs:
name: cov-thread-1-2-backbone name: cov-thread-1-2-backbone
path: tmp/coverage.info path: tmp/coverage.info
thread-border-routing: thread-border-router:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
env: env:
REFERENCE_DEVICE: 1 REFERENCE_DEVICE: 1
@@ -337,15 +337,15 @@ jobs:
run: | run: |
export CI_ENV="$(bash <(curl -s https://codecov.io/env)) -e GITHUB_ACTIONS -e COVERAGE" export CI_ENV="$(bash <(curl -s https://codecov.io/env)) -e GITHUB_ACTIONS -e COVERAGE"
echo "CI_ENV=${CI_ENV}" 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 - uses: actions/upload-artifact@v2
with: with:
name: cov-thread-border-routing-docker name: cov-thread-border-router-docker
path: /tmp/coverage/ path: /tmp/coverage/
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: ${{ failure() }} if: ${{ failure() }}
with: with:
name: thread-border-routing-results name: thread-border-router-results
path: | path: |
*.pcap *.pcap
*.json *.json
@@ -355,7 +355,7 @@ jobs:
./script/test generate_coverage gcc ./script/test generate_coverage gcc
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
with: with:
name: cov-thread-border-routing name: cov-thread-border-router
path: tmp/coverage.info path: tmp/coverage.info
upload-coverage: upload-coverage:
@@ -365,7 +365,7 @@ jobs:
- packet-verification-1-1-on-1-2 - packet-verification-1-1-on-1-2
- expects - expects
- thread-1-2-backbone - thread-1-2-backbone
- thread-border-routing - thread-border-router
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
+1 -1
View File
@@ -255,7 +255,7 @@ do_build_otbr_docker()
echo "Building OTBR Docker ..." echo "Building OTBR Docker ..."
local otdir local otdir
local otbrdir 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} local otbr_docker_image=${OTBR_DOCKER_IMAGE:-otbr-ot12-backbone-ci}
# Always enable SRP server for OTBR. # Always enable SRP server for OTBR.
+1
View File
@@ -1254,6 +1254,7 @@ void Server::HandleOutstandingUpdatesTimer(Timer &aTimer)
void Server::HandleOutstandingUpdatesTimer(void) void Server::HandleOutstandingUpdatesTimer(void)
{ {
otLogInfoSrp("[server] outstanding service update timeout");
while (!mOutstandingUpdates.IsEmpty() && mOutstandingUpdates.GetTail()->GetExpireTime() <= TimerMilli::GetNow()) while (!mOutstandingUpdates.IsEmpty() && mOutstandingUpdates.GetTail()->GetExpireTime() <= TimerMilli::GetNow())
{ {
HandleAdvertisingResult(mOutstandingUpdates.GetTail(), OT_ERROR_RESPONSE_TIMEOUT); HandleAdvertisingResult(mOutstandingUpdates.GetTail(), OT_ERROR_RESPONSE_TIMEOUT);
@@ -91,6 +91,11 @@ class BBR_5_11_01(thread_cert.TestCase):
self.simulator.go(5) self.simulator.go(5)
self.assertEqual('router', self.nodes[ROUTER2].get_state()) 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.nodes[BR_1].start()
self.simulator.go(5) self.simulator.go(5)
self.assertEqual('router', self.nodes[BR_1].get_state()) self.assertEqual('router', self.nodes[BR_1].get_state())
@@ -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()
+39
View File
@@ -39,6 +39,7 @@ import time
import traceback import traceback
import unittest import unittest
from typing import Union, Dict, Optional, List from typing import Union, Dict, Optional, List
from zeroconf import Zeroconf, ServiceInfo
import pexpect import pexpect
import pexpect.popen_spawn 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=2')
self.bash(f'sysctl net.ipv6.conf.{self.ETH_DEV}.accept_ra_rt_info_max_plen=64') 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: class OtCli:
@@ -2417,6 +2421,41 @@ class LinuxHost():
(self.ETH_DEV, self.ETH_DEV)) (self.ETH_DEV, self.ETH_DEV))
self.bash(f'ip -6 neigh list 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 '<service-name>.<protocol>'.
: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): class OtbrNode(LinuxHost, NodeImpl, OtbrDocker):
is_otbr = True is_otbr = True
@@ -2,3 +2,4 @@ ipaddress
pexpect pexpect
pycryptodome pycryptodome
pyshark==0.4.2.11 pyshark==0.4.2.11
zeroconf