Files
openthread/tests/nexus/verify_1_4_DNS_TC_3.py
Jonathan Hui 3b84b4c5cb [nexus] implement test 1_4_DNS_TC_3 for upstream DNS resolver selection (#12835)
This commit implements the Nexus test specification 1_4_DNS_TC_3 for
upstream DNS resolver selection in OpenThread.

Nexus Platform Enhancements:
- Added OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE and
  OPENTHREAD_CONFIG_PLATFORM_DNS_ENABLE to nexus config.
- Implemented platform DNS APIs in nexus_dns.cpp, supporting
  upstream server selection based on prefix lifetimes and reachability.
- Added UdpHook to Core to allow tests to intercept and simulate
  responses for backbone UDP traffic on port 53.
- Updated InfraIf::Receive to call Core::HandleUdp for generic UDP
  interception.
- Added raw buffer delivery overloads for InfraIf::SendUdp.

Test Implementation:
- Created test_1_4_DNS_TC_3.cpp which performs network formation,
  RA signaling (PIO/RIO/RDNSS), and DNS resolution triggers.
- Created verify_1_4_DNS_TC_3.py to validate network behavior,
  RA contents, and correct upstream query routing using pktverify.
- Integrated the new test into CMakeLists.txt and the default
  run_nexus_tests.sh suite.
2026-04-08 17:01:03 -05:00

281 lines
11 KiB
Python

#!/usr/bin/env python3
#
# Copyright (c) 2026, 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 sys
import os
# Add the current directory to sys.path to find verify_utils
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(CUR_DIR)
import verify_utils
from pktverify import consts
from pktverify.addrs import Ipv6Addr
GUA1_PREFIX = "2005:1234:abcd:100::/64"
ETH1_ADDR = "2005:1234:abcd:100::e1"
ETH2_ADDR = "2005:1234:abcd:100::e2"
THREADGROUP1 = "threadgroup1.org"
THREADGROUP2 = "threadgroup2.org"
THREADGROUP3 = "threadgroup3.org"
ANSWER1_ETH1 = "2002:1234::e1:1"
ANSWER2_ETH2 = "2002:1234::e2:2"
ANSWER3_ETH1 = "2002:1234::e1:3"
# ICMPv6 RA Option Types
ND_OPTION_PIO = 3
ND_OPTION_RDNSS = 25
def verify(pv):
pkts = pv.pkts
BR_1 = pv.vars['BR_1']
Router_1 = pv.vars['Router_1']
ED_1 = pv.vars['ED_1']
Eth_1 = pv.vars['Eth_1']
Eth_2 = pv.vars['Eth_2']
BR_1_ETH_ADDR = pv.vars['BR_1_ETH']
Eth_1_ETH_ADDR = pv.vars['Eth_1_ETH']
Eth_2_ETH_ADDR = pv.vars['Eth_2_ETH']
# Step 1
# - Device: Eth_1, Eth_2
# - Description (DNS-11.3): Enable
# - Pass Criteria:
# - N/A
print("Step 1: Enable Eth_1, Eth_2")
# Step 2
# - Device: Eth_2
# - Description (DNS-11.3): Harness instructs device to: Configure router
# advertisement daemon (radvd) to multicast ND RA with: Prefix Information
# Option (PIO) with a global on-link prefix GUA_1 (SLAAC is enabled (A=1),
# on-link (L=1)), Recursive DNS Server Option (25), with Addresses of IPv6
# Recursive DNS Servers field contains a single global IPv6 address of Eth_1.
# Configure DNS server with records: threadgroup1.org AAAA 2002:1234::E2:1,
# threadgroup2.org AAAA 2002:1234::E2:2, threadgroup3.org AAAA 2002:1234::E2:3.
# Note: prefix GUA_1 may be 2005:1234:abcd:100::/64.
# - Pass Criteria:
# - N/A
print("Step 2: Eth_2 sends RA with global IPv6 address of Eth_1")
pkts.filter_eth_src(Eth_2_ETH_ADDR).\
filter_icmpv6_nd_ra().\
filter(lambda p: ND_OPTION_PIO in p.icmpv6.opt.type).\
filter(lambda p: ND_OPTION_RDNSS in p.icmpv6.opt.type).\
must_next()
# Step 3
# - Device: Eth_1
# - Description (DNS-11.3): Harness instructs device to: Configure DNS server
# with records: threadgroup1.org AAAA 2002:1234::E1:1, threadgroup2.org AAAA
# 2002:1234::E1:2, threadgroup3.org AAAA 2002:1234::E1:3
# - Pass Criteria:
# - N/A
print("Step 3: Configure DNS records on Eth_1")
# Step 4
# - Device: BR_1 (DUT), Router_1, ED_1
# - Description (DNS-11.3): Enable
# - Pass Criteria:
# - Single Thread Network forms.
print("Step 4: Enable BR_1, Router_1, ED_1")
pkts.filter_wpan_src64(BR_1).\
filter_mle_cmd(consts.MLE_ADVERTISEMENT).\
must_next()
pkts.filter_wpan_src64(Router_1).\
filter_mle_cmd(consts.MLE_CHILD_ID_REQUEST).\
must_next()
pkts.filter_wpan_src64(ED_1).\
filter_mle_cmd(consts.MLE_CHILD_ID_REQUEST).\
must_next()
# Step 5
# - Device: BR_1 (DUT)
# - Description (DNS-11.3): Automatically obtains / configures an OMR prefix for
# the Thread Network, and assigns an address for its AIL interface using SLAAC.
# - Pass Criteria:
# - N/A
print("Step 5: BR_1 configures OMR prefix and AIL address")
# Step 6
# - Device: ED_1
# - Description (DNS-11.3): Harness instructs device to perform DNS query
# Qtype=AAAA, name threadgroup1.org. Automatically, the DNS query gets
# routed to the DUT
# - Pass Criteria:
# - N/A
print("Step 6: ED_1 performs DNS query for threadgroup1.org")
pkts.filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\
must_next()
# Step 7
# - Device: BR_1 (DUT)
# - Description (DNS-11.3): Automatically processes the DNS query by requesting
# upstream to the Eth_1 DNS server. Then, it responds back with the answer
# to ED_1.
# - Pass Criteria:
# - N/A
print("Step 7: BR_1 requests upstream to Eth_1")
pkts.filter_eth_src(BR_1_ETH_ADDR).\
filter(lambda p: p.eth.dst == Eth_1_ETH_ADDR).\
filter_ipv6_dst(ETH1_ADDR).\
filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\
must_next()
# Step 8
# - Device: ED_1
# - Description (DNS-11.3): Successfully receives DNS query result:
# threadgroup1.org AAAA 2002:1234::E1:1
# - Pass Criteria:
# - ED_1 MUST receive correct DNS query answer from the DUT.
print("Step 8: ED_1 receives correct answer")
pkts.filter_eth_src(Eth_1_ETH_ADDR).\
filter(lambda p: p.eth.dst == BR_1_ETH_ADDR).\
must_next()
# Step 9
# - Device: Eth_2
# - Description (DNS-11.3): Harness instructs device to configure router
# advertisement daemon (radvd) to multicast ND RA with updated information,
# pointing to another DNS server: Prefix Information Option (PIO) - as
# before, Recursive DNS Server Option (25), with Addresses of IPv6
# Recursive DNS Servers field contains a single global IPv6 address of
# Eth_2. Harness waits for a time of at least RA_PERIOD + 1 seconds, where
# RA_PERIOD is the max period between multicast ND RA transmissions by
# Eth_2, to ensure that the new ND RA is received by DUT.
# - Pass Criteria:
# - N/A
print("Step 9: Eth_2 sends RA with global IPv6 address of Eth_2")
pkts.filter_eth_src(Eth_2_ETH_ADDR).\
filter_icmpv6_nd_ra().\
filter(lambda p: ND_OPTION_PIO in p.icmpv6.opt.type).\
filter(lambda p: ND_OPTION_RDNSS in p.icmpv6.opt.type).\
must_next()
# Step 10
# - Device: ED_1
# - Description (DNS-11.3): Harness instructs device to perform DNS query
# Qtype=AAAA, name threadgroup2.org. Automatically, the DNS query gets
# routed to the DUT.
# - Pass Criteria:
# - N/A
print("Step 10: ED_1 performs DNS query for threadgroup2.org")
pkts.filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\
must_next()
# Step 11
# - Device: BR_1 (DUT)
# - Description (DNS-11.3): Automatically processes the DNS query by requesting
# upstream to the Eth_2 DNS server. Then, it responds back with the answer
# to ED_1.
# - Pass Criteria:
# - N/A
print("Step 11: BR_1 requests upstream to Eth_2")
pkts.filter_eth_src(BR_1_ETH_ADDR).\
filter(lambda p: p.eth.dst == Eth_2_ETH_ADDR).\
filter_ipv6_dst(ETH2_ADDR).\
filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\
must_next()
# Step 12
# - Device: ED_1
# - Description (DNS-11.3): Successfully receives DNS query result:
# threadgroup2.org AAAA 2002:1234::E2:2
# - Pass Criteria:
# - ED_1 MUST receive correct DNS query answer from the DUT.
print("Step 12: ED_1 receives correct answer")
pkts.filter_eth_src(Eth_2_ETH_ADDR).\
filter(lambda p: p.eth.dst == BR_1_ETH_ADDR).\
must_next()
# Step 13
# - Device: Eth_2
# - Description (DNS-11.3): Harness instructs device to configure router
# advertisement daemon (radvd) to multicast ND RA with updated information,
# pointing to the first DNS server using link-local address: Prefix
# Information Option (PIO) - as before, Recursive DNS Server Option (25),
# with Addresses of IPv6 Recursive DNS Servers field contains a single
# link-local IPv6 address of Eth_1. Harness waits for a time of at least
# RA_PERIOD + 1 seconds, where RA_PERIOD is the max period between
# multicast ND RA transmissions by Eth_2, to ensure that the new RA is
# received by DUT.
# - Pass Criteria:
# - N/A
print("Step 13: Eth_2 sends RA with link-local IPv6 address of Eth_1")
pkts.filter_eth_src(Eth_2_ETH_ADDR).\
filter_icmpv6_nd_ra().\
filter(lambda p: ND_OPTION_PIO in p.icmpv6.opt.type).\
filter(lambda p: ND_OPTION_RDNSS in p.icmpv6.opt.type).\
must_next()
# Step 14
# - Device: ED_1
# - Description (DNS-11.3): Harness instructs device to perform DNS query
# Qtype=AAAA, name threadgroup3.org. Automatically, the DNS query gets
# routed to the DUT
# - Pass Criteria:
# - N/A
print("Step 14: ED_1 performs DNS query for threadgroup3.org")
pkts.filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\
must_next()
# Step 15
# - Device: BR_1 (DUT)
# - Description (DNS-11.3): Automatically processes the DNS query by requesting
# upstream to the Eth_1 DNS server. Then, it responds back with the answer
# to ED_1.
# - Pass Criteria:
# - N/A
print("Step 15: BR_1 requests upstream to Eth_1")
pkts.filter_eth_src(BR_1_ETH_ADDR).\
filter(lambda p: p.eth.dst == Eth_1_ETH_ADDR).\
filter(lambda p: 'udp' in p.layer_names and p.udp.dstport == 53).\
must_next()
# Step 16
# - Device: ED_1
# - Description (DNS-11.3): Successfully receives DNS query result:
# threadgroup3.org AAAA 2002:1234::E1:3
# - Pass Criteria:
# - ED_1 MUST receive correct DNS query answer from the DUT.
print("Step 16: ED_1 receives correct answer")
pkts.filter_eth_src(Eth_1_ETH_ADDR).\
filter(lambda p: p.eth.dst == BR_1_ETH_ADDR).\
must_next()
if __name__ == '__main__':
verify_utils.run_main(verify)