diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index 60ea61085..9ca00ee15 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -138,6 +138,7 @@ run test-032-child-attach-with-multiple-ip-addresses.py run test-033-mesh-local-prefix-change.py run test-034-poor-link-parent-child-attach.py run test-035-child-timeout-large-data-poll.py +run test-036-wpantund-host-route-management.py run test-100-mcu-power-state.py run test-600-channel-manager-properties.py run test-601-channel-manager-channel-change.py diff --git a/tests/toranj/test-036-wpantund-host-route-management.py b/tests/toranj/test-036-wpantund-host-route-management.py new file mode 100644 index 000000000..9049835b2 --- /dev/null +++ b/tests/toranj/test-036-wpantund-host-route-management.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python +# +# Copyright (c) 2019, 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 wpan +from wpan import verify + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# This test covers behavior of wpantund feature for managing of host interface routes (related to off-mesh routes +# within the Thread network). This feature can be enabled using "Daemon:OffMeshRoute:AutoAddOnInterface" property (is +# enabled by default). +# +# A route corresponding to an off-mesh route would be added on host primary interface (by wpantund), +# if it is added by at least one (other) device within the network and +# (a) either it is not added by host/this-device, or +# (b) if it is also added by the device itself then +# - filtering of self added routes is not enabled, and +# - it is added at lower preference level. + + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Utility functions + + +def verify_interface_routes(node, route_list): + """ + This function verifies that node has the same interface routes as given by `route_list` which is an array of + tuples of (route, prefix_len, metric). + """ + node_routes = wpan.parse_interface_routes_result(node.get(wpan.WPAN_IP6_INTERFACE_ROUTES)) + + verify(len(route_list) == len(node_routes)) + + for route in route_list: + for node_route in node_routes: + if (node_route.route_prefix, node_route.prefix_len, node_route.metric) == route: + break + else: + raise wpan.VerifyError( + 'Did not find route {} on node {}'.format(route, node) + ) + + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +speedup = 4 +wpan.Node.set_time_speedup_factor(speedup) + +r1 = wpan.Node() +r2 = wpan.Node() +r3 = wpan.Node() +c3 = wpan.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +# ----------------------------------------------------------------------------------------------------------------------- +# Build network topology +# +# Test topology: +# +# r1 ---- r2 +# \ / +# \ / +# \ / +# r3 ---- c3 +# +# 3 routers, c3 is added to ensure r3 is promoted to a router quickly! + + +r1.form("route-test") + +r1.whitelist_node(r2) +r2.whitelist_node(r1) +r2.join_node(r1, wpan.JOIN_TYPE_ROUTER) + +r3.whitelist_node(r2) +r2.whitelist_node(r3) +r3.join_node(r2, wpan.JOIN_TYPE_ROUTER) + +c3.whitelist_node(r3) +r3.whitelist_node(c3) +c3.join_node(r3, wpan.JOIN_TYPE_END_DEVICE) + +r3.whitelist_node(r1) +r1.whitelist_node(r3) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +ROUTE1 = 'fd00:abba::' +LEN1 = 64 + +ROUTE2 = 'fd00:cafe:feed::' +LEN2 = 64 + +ROUTE3 = 'fd00:abba::' +LEN3 = 48 + +ROUTE4 = 'fd00:1234::' +LEN4 = 64 + +# Route Priority for off-mesh routes +HIGH_PRIORITY = 1 +MEDIUM_PRIORITY = 0 +LOW_PRIORITY = -1 + +# Host route metric mapping to off-mesh route (note lower metric value is higher priority) +HIGH_METRIC = 1 +MEDIUM_METRIC = 256 +LOW_METRIC = 512 + +WAIT_TIME = 10 + +# Verify the default daemon configuration for managing host/off-mesh routes +verify(r1.get(wpan.WPAN_DAEMON_OFF_MESH_ROUTE_AUTO_ADD_ON_INTERFACE) == 'true') +verify(r1.get(wpan.WPAN_DAEMON_OFF_MESH_ROUTE_FILTER_SELF_AUTO_ADDED) == 'true') + +# Disable the auto route add on r2. +r2.set(wpan.WPAN_DAEMON_OFF_MESH_ROUTE_AUTO_ADD_ON_INTERFACE, 'false') + +# Verify the host interface routes are empty when we start. +verify_interface_routes(r1, []) + +# Add all 3 routes on r2. +r2.add_route(ROUTE1, prefix_len=LEN1, priority=LOW_PRIORITY) +r2.add_route(ROUTE2, prefix_len=LEN2, priority=MEDIUM_PRIORITY) +r2.add_route(ROUTE3, prefix_len=LEN3, priority=HIGH_PRIORITY) + + +# We expect to see all 3 routes added on r1 host interface with same priority levels as r2. +def check_routes_on_r1_1(): + verify_interface_routes(r1, + [(ROUTE1, LEN1, LOW_METRIC), (ROUTE2, LEN2, MEDIUM_METRIC), (ROUTE3, LEN3, HIGH_METRIC)]) + + +wpan.verify_within(check_routes_on_r1_1, WAIT_TIME) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# Add the same routes on r3 with different priorities. +r3.add_route(ROUTE1, prefix_len=LEN1, priority=MEDIUM_PRIORITY) +r3.add_route(ROUTE2, prefix_len=LEN2, priority=LOW_PRIORITY) + + +# We expect the host interface routes on r1 to change accordingly +def check_routes_on_r1_2(): + route_list = [(ROUTE1, LEN1, MEDIUM_METRIC), (ROUTE2, LEN2, MEDIUM_METRIC), (ROUTE3, LEN3, HIGH_METRIC)] + verify_interface_routes(r1, route_list) + + +wpan.verify_within(check_routes_on_r1_2, WAIT_TIME) +verify_interface_routes(r2, []) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# Remove the previously added routes from r2. +r2.remove_route(ROUTE1, prefix_len=LEN1) +r2.remove_route(ROUTE2, prefix_len=LEN2) +r2.remove_route(ROUTE3, prefix_len=LEN3) + + +# We expect the host interface routes on r1 to again change accordingly: +def check_routes_on_r1_3(): + verify_interface_routes(r1, [(ROUTE1, LEN1, MEDIUM_METRIC), (ROUTE2, LEN2, LOW_METRIC)]) + + +wpan.verify_within(check_routes_on_r1_3, WAIT_TIME) +verify_interface_routes(r2, []) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# Disable "Daemon:OffMeshRoute:FilterSelfAutoAdded" feature on wpantund. +# +# The route should be added on host primary interface, if it +# is added by at least one other device within the network and, +# (a) either it is not added by host/this-device, or +# (b) if it is also added by device then +# - filtering of self added routes is not enabled, and +# - it is added at lower preference level. + +r1.set(wpan.WPAN_DAEMON_OFF_MESH_ROUTE_FILTER_SELF_AUTO_ADDED, 'false') +verify(r1.get(wpan.WPAN_DAEMON_OFF_MESH_ROUTE_FILTER_SELF_AUTO_ADDED) == 'false') + +# Add ROUTE1 on r1 with low-priority. Since it's also present on r3 with +# medium priority, we should still see the route on host (as medium). + +r1.add_route(ROUTE1, prefix_len=LEN1, priority=LOW_PRIORITY) + +verify_interface_routes(r1, [(ROUTE1, LEN1, MEDIUM_METRIC), (ROUTE2, LEN2, LOW_METRIC)]) + +# Now change ROUTE1 on r1 to be same priority as on r2, now the route should +# no longer be present on host interface routes. + +r1.remove_route(ROUTE1, prefix_len=LEN1) +r1.add_route(ROUTE1, prefix_len=LEN1, priority=MEDIUM_PRIORITY) + +verify_interface_routes(r1, [(ROUTE2, LEN2, LOW_METRIC)]) + +# Adding ROUTE2 with higher priority should remove it from interface routes +r1.add_route(ROUTE2, prefix_len=LEN2, priority=MEDIUM_PRIORITY) + +verify_interface_routes(r1, []) + +# Adding a new ROUTE4 on r1 should not change anything related to interface host routes. +r1.add_route(ROUTE4, prefix_len=LEN4, priority=MEDIUM_METRIC) +verify_interface_routes(r1, []) + +# Removing ROUTE1 and ROUT2 on r1 should cause them to be added back on host +# interface (since they are still present as off-mesh routes on r3). +r1.remove_route(ROUTE1, prefix_len=LEN1) +r1.remove_route(ROUTE2, prefix_len=LEN2) + +verify_interface_routes(r1, [(ROUTE1, LEN1, MEDIUM_METRIC), (ROUTE2, LEN2, LOW_METRIC)]) + +verify_interface_routes(r2, []) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Enable "Daemon:OffMeshRoute:FilterSelfAutoAdded" feature on wpantund. + +r1.set(wpan.WPAN_DAEMON_OFF_MESH_ROUTE_FILTER_SELF_AUTO_ADDED, 'true') +verify(r1.get(wpan.WPAN_DAEMON_OFF_MESH_ROUTE_FILTER_SELF_AUTO_ADDED) == 'true') + +# Adding ROUTE1 with any priority should remove it from host interface routes. +r1.add_route(ROUTE1, prefix_len=LEN1, priority=LOW_PRIORITY) + +verify_interface_routes(r1, [(ROUTE2, LEN2, LOW_METRIC)]) + +r1.remove_route(ROUTE1, prefix_len=LEN1) + +verify_interface_routes(r1, [(ROUTE1, LEN1, MEDIUM_METRIC), (ROUTE2, LEN2, LOW_METRIC)]) + +verify_interface_routes(r2, []) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +wpan.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/wpan.py b/tests/toranj/wpan.py index b16889a79..f0dd3fac2 100644 --- a/tests/toranj/wpan.py +++ b/tests/toranj/wpan.py @@ -65,6 +65,10 @@ WPAN_IP6_MESH_LOCAL_ADDRESS = "IPv6:MeshLocalAddress" WPAN_IP6_MESH_LOCAL_PREFIX = "IPv6:MeshLocalPrefix" WPAN_IP6_ALL_ADDRESSES = "IPv6:AllAddresses" WPAN_IP6_MULTICAST_ADDRESSES = "IPv6:MulticastAddresses" +WPAN_IP6_INTERFACE_ROUTES = "IPv6:Routes" + +WPAN_DAEMON_OFF_MESH_ROUTE_AUTO_ADD_ON_INTERFACE = "Daemon:OffMeshRoute:AutoAddOnInterface" +WPAN_DAEMON_OFF_MESH_ROUTE_FILTER_SELF_AUTO_ADDED = "Daemon:OffMeshRoute:FilterSelfAutoAdded" WPAN_THREAD_RLOC16 = "Thread:RLOC16" WPAN_THREAD_ROUTER_ID = "Thread:RouterID" @@ -1569,3 +1573,49 @@ def parse_address_cache_table_result(addr_cache_table_list): """ Parses address cache table list string and returns an array of `AddressCacheEntry` objects""" return [AddressCacheEntry(item) for item in addr_cache_table_list.split('\n')[1:-1]] + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +class InterfaceRoute(object): + """ This object encapsulates an interface route entry""" + + def __init__(self, text): + + # Example of expected text: + # + # '\t"fd00:abba::/64 metric:256 "' + # + + # We get rid of the first two chars `\t"' and last char '"', split the rest using whitespace as separator. + # Then remove any ',' at end of items in the list. + items = [ + item[:-1] if item[-1] == ',' else item + for item in text[2:-1].split() + ] + + # First item in the extended address + self._route_prefix = items[0].split('/')[0] + self._prefix_len = int(items[0].split('/')[1], 0) + self._metric = int(items[1].split(':')[1], 0) + + @property + def route_prefix(self): + return self._route_prefix + + @property + def prefix_len(self): + return self._prefix_len + + @property + def metric(self): + return self._metric + + def __repr__(self): + return 'InterfaceRoute({})'.format(self.__dict__) + + +def parse_interface_routes_result(interface_routes_list): + """ Parses interface routes list string and returns an array of `InterfaceRoute` objects""" + return [InterfaceRoute(item) + for item in interface_routes_list.split('\n')[1:-1]]