diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 108506205..8c3b4cfee 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 (134) +#define OPENTHREAD_API_VERSION (135) /** * @addtogroup api-instance diff --git a/include/openthread/ip6.h b/include/openthread/ip6.h index c2fd39485..33bf301cc 100644 --- a/include/openthread/ip6.h +++ b/include/openthread/ip6.h @@ -561,6 +561,18 @@ const uint16_t *otIp6GetUnsecurePorts(otInstance *aInstance, uint8_t *aNumEntrie */ bool otIp6IsAddressEqual(const otIp6Address *aFirst, const otIp6Address *aSecond); +/** + * Test if two IPv6 prefixes are the same. + * + * @param[in] aFirst A pointer to the first IPv6 prefix to compare. + * @param[in] aSecond A pointer to the second IPv6 prefix to compare. + * + * @retval TRUE The two IPv6 prefixes are the same. + * @retval FALSE The two IPv6 prefixes are not the same. + * + */ +bool otIp6ArePrefixesEqual(const otIp6Prefix *aFirst, const otIp6Prefix *aSecond); + /** * This function converts a human-readable IPv6 address string into a binary representation. * diff --git a/src/core/api/ip6_api.cpp b/src/core/api/ip6_api.cpp index 9ec4f5359..5263dc3e1 100644 --- a/src/core/api/ip6_api.cpp +++ b/src/core/api/ip6_api.cpp @@ -227,6 +227,11 @@ bool otIp6IsAddressEqual(const otIp6Address *aFirst, const otIp6Address *aSecond return *static_cast(aFirst) == *static_cast(aSecond); } +bool otIp6ArePrefixesEqual(const otIp6Prefix *aFirst, const otIp6Prefix *aSecond) +{ + return *static_cast(aFirst) == *static_cast(aSecond); +} + otError otIp6AddressFromString(const char *aString, otIp6Address *aAddress) { return static_cast(aAddress)->FromString(aString); diff --git a/src/posix/platform/netif.cpp b/src/posix/platform/netif.cpp index 55c7079aa..d2d1956e2 100644 --- a/src/posix/platform/netif.cpp +++ b/src/posix/platform/netif.cpp @@ -141,6 +141,7 @@ extern int #include #include #include +#include #include #include "common/code_utils.hpp" @@ -189,6 +190,17 @@ using namespace ot::Posix::Ip6Utils; static uint32_t sNetlinkSequence = 0; ///< Netlink message sequence. #endif +#if OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE +#if defined(__linux__) +static constexpr uint32_t kExternalRoutePriority = OPENTHREAD_POSIX_CONFIG_EXTERNAL_ROUTE_PRIORITY; +static constexpr uint8_t kMaxExternalRoutesNum = OPENTHREAD_POSIX_CONFIG_MAX_EXTERNAL_ROUTE_NUM; +static uint8_t sAddedExternalRoutesNum = 0; +static otIp6Prefix sAddedExternalRoutes[kMaxExternalRoutesNum]; +#else +#error "OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE only works on Linux platform" +#endif // defined(__linux__) +#endif + #if defined(RTM_NEWMADDR) || defined(__NetBSD__) // on some BSDs (mac OS, FreeBSD), we get RTM_NEWMADDR/RTM_DELMADDR messages, so we don't need to monitor using MLD // on NetBSD, MLD monitoring simply doesn't work @@ -505,6 +517,212 @@ exit: } } +#if OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE +void AddRtAttr(struct nlmsghdr *aHeader, uint32_t aMaxLen, uint8_t aType, const void *aData, uint8_t aLen) +{ + uint8_t len = RTA_LENGTH(aLen); + struct rtattr *rta; + + assert(NLMSG_ALIGN(aHeader->nlmsg_len) + RTA_ALIGN(len) <= aMaxLen); + OT_UNUSED_VARIABLE(aMaxLen); + + rta = (struct rtattr *)((char *)(aHeader) + NLMSG_ALIGN((aHeader)->nlmsg_len)); + rta->rta_type = aType; + rta->rta_len = len; + if (aLen) + { + memcpy(RTA_DATA(rta), aData, aLen); + } + aHeader->nlmsg_len = NLMSG_ALIGN(aHeader->nlmsg_len) + RTA_ALIGN(len); +} + +void AddRtAttrUint32(struct nlmsghdr *aHeader, uint32_t aMaxLen, uint8_t aType, uint32_t aData) +{ + AddRtAttr(aHeader, aMaxLen, aType, &aData, sizeof(aData)); +} + +static otError AddExternalRoute(const otIp6Prefix &aPrefix) +{ + constexpr unsigned int kBufSize = 128; + struct + { + struct nlmsghdr header; + struct rtmsg msg; + char buf[kBufSize]; + } req{}; + unsigned char data[sizeof(in6_addr)]; + char addrBuf[OT_IP6_ADDRESS_STRING_SIZE]; + unsigned int netifIdx = otSysGetThreadNetifIndex(); + otError error = OT_ERROR_NONE; + + VerifyOrExit(netifIdx > 0, error = OT_ERROR_INVALID_STATE); + VerifyOrExit(sNetlinkFd >= 0, error = OT_ERROR_INVALID_STATE); + VerifyOrExit(sAddedExternalRoutesNum < kMaxExternalRoutesNum, error = OT_ERROR_NO_BUFS); + + req.header.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; + + req.header.nlmsg_len = NLMSG_LENGTH(sizeof(rtmsg)); + req.header.nlmsg_type = RTM_NEWROUTE; + req.header.nlmsg_pid = 0; + req.header.nlmsg_seq = ++sNetlinkSequence; + + req.msg.rtm_family = AF_INET6; + req.msg.rtm_src_len = 0; + req.msg.rtm_dst_len = aPrefix.mLength; + req.msg.rtm_tos = 0; + req.msg.rtm_scope = RT_SCOPE_UNIVERSE; + req.msg.rtm_type = RTN_UNICAST; + req.msg.rtm_table = RT_TABLE_MAIN; + req.msg.rtm_protocol = RTPROT_BOOT; + req.msg.rtm_flags = 0; + + otIp6AddressToString(&aPrefix.mPrefix, addrBuf, OT_IP6_ADDRESS_STRING_SIZE); + inet_pton(AF_INET6, addrBuf, data); + AddRtAttr(&req.header, sizeof(req), RTA_DST, data, sizeof(data)); + AddRtAttrUint32(&req.header, sizeof(req), RTA_PRIORITY, kExternalRoutePriority); + AddRtAttrUint32(&req.header, sizeof(req), RTA_OIF, netifIdx); + + if (send(sNetlinkFd, &req, sizeof(req), 0) < 0) + { + VerifyOrExit(errno == EAGAIN || errno == EINTR || errno == EWOULDBLOCK, error = OT_ERROR_BUSY); + DieNow(OT_EXIT_ERROR_ERRNO); + } +exit: + return error; +} + +static otError DeleteExternalRoute(const otIp6Prefix &aPrefix) +{ + constexpr unsigned int kBufSize = 512; + struct + { + struct nlmsghdr header; + struct rtmsg msg; + char buf[kBufSize]; + } req{}; + unsigned char data[sizeof(in6_addr)]; + char addrBuf[OT_IP6_ADDRESS_STRING_SIZE]; + unsigned int netifIdx = otSysGetThreadNetifIndex(); + otError error = OT_ERROR_NONE; + + VerifyOrExit(netifIdx > 0, error = OT_ERROR_INVALID_STATE); + VerifyOrExit(sNetlinkFd >= 0, error = OT_ERROR_INVALID_STATE); + + req.header.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_NONREC; + + req.header.nlmsg_len = NLMSG_LENGTH(sizeof(rtmsg)); + req.header.nlmsg_type = RTM_DELROUTE; + req.header.nlmsg_pid = 0; + req.header.nlmsg_seq = ++sNetlinkSequence; + + req.msg.rtm_family = AF_INET6; + req.msg.rtm_src_len = 0; + req.msg.rtm_dst_len = aPrefix.mLength; + req.msg.rtm_tos = 0; + req.msg.rtm_scope = RT_SCOPE_UNIVERSE; + req.msg.rtm_type = RTN_UNICAST; + req.msg.rtm_table = RT_TABLE_MAIN; + req.msg.rtm_protocol = RTPROT_BOOT; + req.msg.rtm_flags = 0; + + otIp6AddressToString(&aPrefix.mPrefix, addrBuf, OT_IP6_ADDRESS_STRING_SIZE); + inet_pton(AF_INET6, addrBuf, data); + AddRtAttr(&req.header, sizeof(req), RTA_DST, data, sizeof(data)); + AddRtAttrUint32(&req.header, sizeof(req), RTA_OIF, netifIdx); + + if (send(sNetlinkFd, &req, sizeof(req), 0) < 0) + { + VerifyOrExit(errno == EAGAIN || errno == EINTR || errno == EWOULDBLOCK, error = OT_ERROR_BUSY); + DieNow(OT_EXIT_ERROR_ERRNO); + } + +exit: + return error; +} + +bool HasExternalRouteInNetData(otInstance *aInstance, const otIp6Prefix &aExternalRoute) +{ + otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT; + otExternalRouteConfig config; + bool found = false; + + while (otNetDataGetNextRoute(aInstance, &iterator, &config) == OT_ERROR_NONE) + { + if (otIp6ArePrefixesEqual(&config.mPrefix, &aExternalRoute)) + { + found = true; + break; + } + } + return found; +} + +bool HasAddedExternalRoute(const otIp6Prefix &aExternalRoute) +{ + bool found = false; + + for (uint8_t i = 0; i < sAddedExternalRoutesNum; ++i) + { + if (otIp6ArePrefixesEqual(&sAddedExternalRoutes[i], &aExternalRoute)) + { + found = true; + break; + } + } + return found; +} + +static void UpdateExternalRoutes(otInstance *aInstance) +{ + otError error; + otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT; + otExternalRouteConfig config; + char prefixString[OT_IP6_PREFIX_STRING_SIZE]; + + for (int i = 0; i < static_cast(sAddedExternalRoutesNum); ++i) + { + if (HasExternalRouteInNetData(aInstance, sAddedExternalRoutes[i])) + { + continue; + } + if ((error = DeleteExternalRoute(sAddedExternalRoutes[i])) != OT_ERROR_NONE) + { + otIp6PrefixToString(&sAddedExternalRoutes[i], prefixString, sizeof(prefixString)); + otLogWarnPlat("failed to delete an external route %s in kernel: %s", prefixString, + otThreadErrorToString(error)); + } + else + { + sAddedExternalRoutes[i] = sAddedExternalRoutes[sAddedExternalRoutesNum - 1]; + --sAddedExternalRoutesNum; + --i; + } + } + + while (otNetDataGetNextRoute(aInstance, &iterator, &config) == OT_ERROR_NONE) + { + if (config.mRloc16 == otThreadGetRloc16(aInstance) || HasAddedExternalRoute(config.mPrefix)) + { + continue; + } + VerifyOrExit(sAddedExternalRoutesNum < kMaxExternalRoutesNum, + otLogWarnPlat("no buffer to add more external routes in kernel")); + if ((error = AddExternalRoute(config.mPrefix)) != OT_ERROR_NONE) + { + otIp6PrefixToString(&config.mPrefix, prefixString, sizeof(prefixString)); + otLogWarnPlat("failed to add an external route %s in kernel: %s", prefixString, + otThreadErrorToString(error)); + } + else + { + sAddedExternalRoutes[sAddedExternalRoutesNum++] = config.mPrefix; + } + } +exit: + return; +} +#endif // OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE + static void processAddressChange(const otIp6AddressInfo *aAddressInfo, bool aIsAdded, void *aContext) { if (aAddressInfo->mAddress->mFields.m8[0] == 0xff) @@ -523,6 +741,12 @@ void platformNetifStateChange(otInstance *aInstance, otChangedFlags aFlags) { UpdateLink(aInstance); } +#if OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE + if (OT_CHANGED_THREAD_NETDATA & aFlags) + { + UpdateExternalRoutes(aInstance); + } +#endif } static void processReceive(otMessage *aMessage, void *aContext) diff --git a/src/posix/platform/openthread-posix-config.h b/src/posix/platform/openthread-posix-config.h index 229b67b33..9bd6d70f6 100644 --- a/src/posix/platform/openthread-posix-config.h +++ b/src/posix/platform/openthread-posix-config.h @@ -155,6 +155,38 @@ #define OPENTHREAD_POSIX_CONFIG_SECURE_SETTINGS_ENABLE 0 #endif +/** + * @def OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE + * + * Define as 1 to add external routes to POSIX kernel when external routes are changed in netdata. + * + */ +#ifdef __linux__ +#ifndef OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE +#define OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE 1 +#endif +#endif + +/** + * @def OPENTHREAD_POSIX_CONFIG_EXTERNAL_ROUTE_PRIORITY + * + * This macro defines the priority of external routes added to kernel. + * + */ +#ifndef OPENTHREAD_POSIX_CONFIG_EXTERNAL_ROUTE_PRIORITY +#define OPENTHREAD_POSIX_CONFIG_EXTERNAL_ROUTE_PRIORITY 512 +#endif + +/** + * @def OPENTHREAD_POSIX_CONFIG_MAX_EXTERNAL_ROUTE_NUM + * + * This macro defines the max number of external routes that can be added to kernel. + * + */ +#ifndef OPENTHREAD_POSIX_CONFIG_MAX_EXTERNAL_ROUTE_NUM +#define OPENTHREAD_POSIX_CONFIG_MAX_EXTERNAL_ROUTE_NUM 8 +#endif + #ifdef __APPLE__ /** diff --git a/tests/scripts/thread-cert/border_router/test_external_route.py b/tests/scripts/thread-cert/border_router/test_external_route.py new file mode 100644 index 000000000..b744d6d45 --- /dev/null +++ b/tests/scripts/thread-cert/border_router/test_external_route.py @@ -0,0 +1,168 @@ +#!/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 ipaddress +import config +import thread_cert + +# Test description: +# This test verifies that when external routes are changed in Network Data, a +# border router will make according updates to routes at the linux kernel. +# +# Topology: +# ----------------(eth)---------------------- +# | | | +# BR1 (Leader) ----- BR2 HOST +# | | +# ROUTER1 ROUTER2 + +BR1 = 1 +BR2 = 2 +ROUTER1 = 3 +ROUTER2 = 4 +HOST = 5 + +ROUTE1 = '2402:1234:1234:1234::/64' + + +class MultiBorderRouters(thread_cert.TestCase): + USE_MESSAGE_FACTORY = False + + TOPOLOGY = { + BR1: { + 'name': 'BR_1', + 'allowlist': [BR2, ROUTER1], + 'is_otbr': True, + 'version': '1.2', + }, + BR2: { + 'name': 'BR_2', + 'allowlist': [BR1, ROUTER2], + 'is_otbr': True, + 'version': '1.2', + }, + ROUTER1: { + 'name': 'Router_1', + 'allowlist': [BR1], + 'version': '1.2', + }, + ROUTER2: { + 'name': 'Router_2', + 'allowlist': [BR2], + 'version': '1.2', + }, + HOST: { + 'name': 'Host', + 'is_host': True, + } + } + + def test(self): + br1 = self.nodes[BR1] + br2 = self.nodes[BR2] + router1 = self.nodes[ROUTER1] + router2 = self.nodes[ROUTER2] + host = self.nodes[HOST] + + br1.start() + self.simulator.go(5) + self.assertEqual('leader', br1.get_state()) + + br2.start() + self.simulator.go(5) + self.assertEqual('router', br2.get_state()) + + router1.start() + router2.start() + host.start() + self.simulator.go(5) + self.assertEqual('router', router1.get_state()) + self.assertEqual('router', router2.get_state()) + + # Manually add ROUTE1 at BR1 + br1.add_route(ROUTE1) + br1.register_netdata() + + # BRs has installed all external routes in their kernels respectively + netdata = br1.get_netdata() + br1_kernel_routes = self.get_routes_from_kernel(br1) + for route in self.get_routes_from_netdata(netdata, br1.get_addr16()): + self.assertIn(route, br1_kernel_routes) + br2_kernel_routes = self.get_routes_from_kernel(br2) + for route in self.get_routes_from_netdata(netdata, br2.get_addr16()): + self.assertIn(route, br2_kernel_routes) + + # ROUTE1 has been installed in BR2 but not BR1 + self.assertNotIn(ipaddress.IPv6Network(ROUTE1), br1_kernel_routes) + self.assertIn(ipaddress.IPv6Network(ROUTE1), br2_kernel_routes) + + # Remove ROUTE1 + br1.remove_route(ROUTE1) + br1.register_netdata() + + # Verify that external routes are still in kernel routes + netdata = br1.get_netdata() + br1_kernel_routes = self.get_routes_from_kernel(br1) + for route in self.get_routes_from_netdata(netdata, br1.get_addr16()): + self.assertIn(route, br1_kernel_routes) + br2_kernel_routes = self.get_routes_from_kernel(br2) + for route in self.get_routes_from_netdata(netdata, br2.get_addr16()): + self.assertIn(route, br2_kernel_routes) + + # ROUTE1 has been removed from kernel routes + self.assertNotIn(ipaddress.IPv6Network(ROUTE1), br1_kernel_routes) + self.assertNotIn(ipaddress.IPv6Network(ROUTE1), br2_kernel_routes) + + br2.bash('ip link set eth0 down') + self.simulator.go(10) + + # HOST pings BR2's OMR, the ping should succeed + self.assertTrue(host.ping_ether(br2.get_ip6_address(address_type=config.ADDRESS_TYPE.OMR)[0])) + + def get_routes_from_netdata(self, netdata, exclude_rloc16): + routes = [] + for entry in netdata['Routes']: + items = entry.split() + prefix = items[0] + rloc16 = int(items[-1], 16) + if rloc16 != exclude_rloc16: + routes.append(ipaddress.IPv6Network(prefix)) + return routes + + def get_routes_from_kernel(self, br): + routes = [] + for entry in br.bash('ip -6 -d route list dev wpan0'): + routes.append(ipaddress.IPv6Network(entry.split()[1])) + return routes + + +if __name__ == '__main__': + unittest.main()