From 7a2465f7512b10fefed6dc2cb7b2741215519964 Mon Sep 17 00:00:00 2001 From: whd <7058128+superwhd@users.noreply.github.com> Date: Fri, 23 Apr 2021 03:43:26 +0800 Subject: [PATCH] [cli] support -I {src-addr} for ping command (#6470) --- include/openthread/instance.h | 2 +- include/openthread/ping_sender.h | 1 + src/cli/cli.cpp | 27 ++++ src/core/utils/ping_sender.cpp | 1 + src/core/utils/ping_sender.hpp | 16 ++ tests/scripts/thread-cert/Makefile.am | 2 + tests/scripts/thread-cert/node.py | 7 +- tests/scripts/thread-cert/test_ping.py | 215 +++++++++++++++++++++++++ 8 files changed, 268 insertions(+), 3 deletions(-) create mode 100755 tests/scripts/thread-cert/test_ping.py diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 34ea6828b..1d4393617 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -53,7 +53,7 @@ extern "C" { * @note This number versions both OpenThread platform and user APIs. * */ -#define OPENTHREAD_API_VERSION (105) +#define OPENTHREAD_API_VERSION (106) /** * @addtogroup api-instance diff --git a/include/openthread/ping_sender.h b/include/openthread/ping_sender.h index 9de484ccb..07e5c03ce 100644 --- a/include/openthread/ping_sender.h +++ b/include/openthread/ping_sender.h @@ -107,6 +107,7 @@ typedef void (*otPingSenderStatisticsCallback)(const otPingSenderStatistics *aSt */ typedef struct otPingSenderConfig { + otIp6Address mSource; ///< Source address of the ping. otIp6Address mDestination; ///< Destination address to ping. otPingSenderReplyCallback mReplyCallback; ///< Callback function to report replies (can be NULL if not needed). otPingSenderStatisticsCallback diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index 7efb58d59..a1dd41413 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -3277,6 +3277,33 @@ otError Interpreter::ProcessPing(uint8_t aArgsLength, char *aArgs[]) memset(&config, 0, sizeof(config)); + if (aArgsLength >= 2) + { + if (!strcmp(aArgs[0], "-I")) + { + SuccessOrExit(error = ParseAsIp6Address(aArgs[1], config.mSource)); +#if !OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE + { + bool valid = false; + const otNetifAddress *unicastAddrs = otIp6GetUnicastAddresses(mInstance); + + SuccessOrExit(error = ParseAsIp6Address(aArgs[1], config.mSource)); + for (const otNetifAddress *addr = unicastAddrs; addr; addr = addr->mNext) + { + if (otIp6IsAddressEqual(&addr->mAddress, &config.mSource)) + { + valid = true; + break; + } + } + VerifyOrExit(valid, error = OT_ERROR_INVALID_ARGS); + } +#endif + aArgs += 2; + aArgsLength -= 2; + } + } + SuccessOrExit(error = ParseAsIp6Address(aArgs[0], config.mDestination)); if (aArgsLength > 1) diff --git a/src/core/utils/ping_sender.cpp b/src/core/utils/ping_sender.cpp index 106705722..441f15731 100644 --- a/src/core/utils/ping_sender.cpp +++ b/src/core/utils/ping_sender.cpp @@ -128,6 +128,7 @@ void PingSender::SendPing(void) Message * message = nullptr; Ip6::MessageInfo messageInfo; + messageInfo.SetSockAddr(mConfig.GetSource()); messageInfo.SetPeerAddr(mConfig.GetDestination()); messageInfo.mHopLimit = mConfig.mHopLimit; messageInfo.mAllowZeroHopLimit = mConfig.mAllowZeroHopLimit; diff --git a/src/core/utils/ping_sender.hpp b/src/core/utils/ping_sender.hpp index a9c5a318e..bc7cb5e55 100644 --- a/src/core/utils/ping_sender.hpp +++ b/src/core/utils/ping_sender.hpp @@ -94,6 +94,22 @@ public: friend class PingSender; public: + /** + * This method gets the source IPv6 address of the ping. + * + * @returns The ping source IPv6 address. + * + */ + Ip6::Address &GetSource(void) { return static_cast(mSource); } + + /** + * This method gets the source IPv6 address of the ping. + * + * @returns The ping source IPv6 address. + * + */ + const Ip6::Address &GetSource(void) const { return static_cast(mSource); } + /** * This method gets the destination IPv6 address to ping. * diff --git a/tests/scripts/thread-cert/Makefile.am b/tests/scripts/thread-cert/Makefile.am index 4af625262..fa9de8e4c 100644 --- a/tests/scripts/thread-cert/Makefile.am +++ b/tests/scripts/thread-cert/Makefile.am @@ -169,6 +169,7 @@ EXTRA_DIST = \ test_mle.py \ test_network_data.py \ test_network_layer.py \ + test_ping.py \ test_reed_address_solicit_rejected.py \ test_reset.py \ test_route_table.py \ @@ -225,6 +226,7 @@ check_SCRIPTS = \ test_mle.py \ test_network_data.py \ test_network_layer.py \ + test_ping.py \ test_reed_address_solicit_rejected.py \ test_reset.py \ test_route_table.py \ diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index 48dc7b6aa..f3019a7f7 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -1766,8 +1766,11 @@ class NodeImpl: return self._expect_results( r'\|\s(\S+)\s+\|\s(\S+)\s+\|\s([0-9a-fA-F]{4})\s\|\s([0-9a-fA-F]{16})\s\|\s(\d+)') - def ping(self, ipaddr, num_responses=1, size=8, timeout=5, count=1, interval=1, hoplimit=64): - cmd = f'ping {ipaddr} {size} {count} {interval} {hoplimit} {timeout}' + def ping(self, ipaddr, num_responses=1, size=8, timeout=5, count=1, interval=1, hoplimit=64, interface=None): + args = f'{ipaddr} {size} {count} {interval} {hoplimit} {timeout}' + if interface is not None: + args = f'-I {interface} {args}' + cmd = f'ping {args}' self.send_command(cmd) diff --git a/tests/scripts/thread-cert/test_ping.py b/tests/scripts/thread-cert/test_ping.py new file mode 100755 index 000000000..1ea1ef833 --- /dev/null +++ b/tests/scripts/thread-cert/test_ping.py @@ -0,0 +1,215 @@ +#!/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 logging +import unittest + +import pktverify +from pktverify import packet_verifier +from pktverify.consts import MA1, MA1g, MA2 +import config +import thread_cert + +# Test description: +# The purpose of this test is to verify the functionality of ping command. +# +# Topology: +# +# +# ROUTER_2 ----- ROUTER_1 ---- ROUTER_3 +# +# + +ROUTER_1 = 1 +ROUTER_2 = 2 +ROUTER_3 = 3 + + +class TestPing(thread_cert.TestCase): + USE_MESSAGE_FACTORY = False + SUPPORT_NCP = False + + TOPOLOGY = { + ROUTER_1: { + 'name': 'Router_1', + 'allowlist': [ROUTER_2, ROUTER_3], + 'router_selection_jitter': 2, + }, + ROUTER_2: { + 'name': 'Router_2', + 'allowlist': [ROUTER_1], + 'router_selection_jitter': 2, + }, + ROUTER_3: { + 'name': 'Router_3', + 'allowlist': [ROUTER_1], + 'router_selection_jitter': 2, + }, + } + + def test(self): + router1 = self.nodes[ROUTER_1] + router2 = self.nodes[ROUTER_2] + router3 = self.nodes[ROUTER_3] + + router1.start() + self.simulator.go(5) + self.assertEqual('leader', router1.get_state()) + + router2.start() + self.simulator.go(5) + self.assertEqual('router', router2.get_state()) + + router3.start() + self.simulator.go(5) + self.assertEqual('router', router3.get_state()) + + # 1. ROUTER_1 pings ROUTER_2. + self.assertTrue(router1.ping(router2.get_ip6_address(config.ADDRESS_TYPE.RLOC))) + + # 2. ROUTER_1 pings ROUTER_2 multiple times. + self.assertTrue(router1.ping(router2.get_ip6_address(config.ADDRESS_TYPE.RLOC), count=5)) + + # 3. ROUTER_2 pings ROUTER_1 from the link-local address to the + # link-local address. + self.assertTrue( + router2.ping(router1.get_ip6_address(config.ADDRESS_TYPE.LINK_LOCAL), + interface=router2.get_ip6_address(config.ADDRESS_TYPE.LINK_LOCAL))) + + # 4. ROUTER_2 pings ROUTER_3 using the RLOC. + self.assertTrue(router2.ping(router3.get_ip6_address(config.ADDRESS_TYPE.RLOC))) + + # 5. ROUTER_2 pings ROUTER_3's link-local address. The ping should fail. + self.assertFalse(router2.ping(router3.get_ip6_address(config.ADDRESS_TYPE.LINK_LOCAL))) + + # 6. ROUTER_2 pings ROUTER_3's RLOC from the link-local address. The + # ping should fail. + self.assertFalse( + router2.ping(router3.get_ip6_address(config.ADDRESS_TYPE.RLOC), + interface=router2.get_ip6_address(config.ADDRESS_TYPE.LINK_LOCAL))) + + # 7. ROUTER_2 pings ROUTER_3's RLOC from an non-existent address. The + # ping command should be rejected by CLI. + self.assertFalse(router2.ping(router3.get_ip6_address(config.ADDRESS_TYPE.RLOC), interface='1::1')) + + self.collect_ipaddrs() + self.collect_rloc16s() + self.collect_rlocs() + self.collect_extra_vars() + + def verify(self, pv: pktverify.packet_verifier.PacketVerifier): + pkts = pv.pkts + vars = pv.vars + pv.summary.show() + + logging.info(f'vars = {vars}') + + # Ensure the topology is formed correctly + pv.verify_attached('Router_2', 'Router_1') + pv.verify_attached('Router_3', 'Router_1') + + # 1. Router_1 pings Router_2. + _pkt = pkts.filter_wpan_src64(vars['Router_1']) \ + .filter_ipv6_2dsts(vars['Router_2_RLOC'], vars['Router_2_LLA']) \ + .filter_ping_request() \ + .must_next() + + pkts.filter_wpan_src64(vars['Router_2']) \ + .filter_ipv6_dst(_pkt.ipv6.src) \ + .filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier) \ + .must_next() + + # 2. Router_1 pings Router_2 multiple times. + for i in range(5): + _pkt = pkts.filter_wpan_src64(vars['Router_1']) \ + .filter_ipv6_2dsts(vars['Router_2_RLOC'], vars['Router_2_LLA']) \ + .filter_ping_request() \ + .must_next() + pkts.filter_wpan_src64(vars['Router_2']) \ + .filter_ipv6_dst(_pkt.ipv6.src) \ + .filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier) \ + .must_next() + + # 3. Router_2 pings Router_1 from the link-local address to the + # link-local address. + _pkt = pkts.filter_wpan_src64(vars['Router_2']) \ + .filter_ipv6_src_dst(vars['Router_2_LLA'], vars['Router_1_LLA']) \ + .filter_ping_request() \ + .must_next() + + pkts.filter_wpan_src64(vars['Router_1']) \ + .filter_ipv6_dst(_pkt.ipv6.src) \ + .filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier) \ + .must_next() + + # 4. Router_2 pings Router_3 using the RLOC. + _pkt = pkts.filter_wpan_src64(vars['Router_2']) \ + .filter_ipv6_dst(vars['Router_3_RLOC']) \ + .filter_ping_request() \ + .must_next() + + pkts.filter_wpan_src64(vars['Router_3']) \ + .filter_ipv6_dst(_pkt.ipv6.src) \ + .filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier) \ + .must_next() + + # 5. Router_2 pings Router_3's link-local address. The ping should fail. + _pkt = pkts.filter_wpan_src64(vars['Router_2']) \ + .filter_ipv6_dst(vars['Router_3_LLA']) \ + .filter_ping_request() \ + .must_next() + + pkts.filter_wpan_src64(vars['Router_3']) \ + .filter_ipv6_dst(_pkt.ipv6.src) \ + .filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier) \ + .must_not_next() + + # 5. Router_2 pings Router_3's RLOC from the link-local address. The + # ping should fail. + _pkt = pkts.filter_wpan_src64(vars['Router_2']) \ + .filter_ipv6_src_dst(vars['Router_2_LLA'], vars['Router_3_RLOC']) \ + .filter_ping_request() \ + .must_next() + + # TODO: Enable this section + # pkts.filter_wpan_src64(vars['Router_3']) \ + # .filter_ipv6_dst(_pkt.ipv6.src) \ + # .filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier) \ + # .must_not_next() + + # 6. Router_2 pings Router_3's RLOC from an non-existent address. The + # ping should fail. + _pkt = pkts.filter_wpan_src64(vars['Router_2']) \ + .filter_ipv6_src_dst('1::1', vars['Router_3_RLOC']) \ + .filter_ping_request() \ + .must_not_next() + + +if __name__ == '__main__': + unittest.main()