diff options
author | Simon Lin <simonlin@google.com> | 2022-07-27 11:34:07 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-26 20:34:07 -0700 |
commit | fe3f8f4c67eac17207df1e887e0adf7d497dd6f3 (patch) | |
tree | e6b2724218f3a5de3359cccb74bb92af4653049c | |
parent | c70d2a03536366fddf4babdbf4cd6ad7a315d151 (diff) | |
download | ot-br-posix-fe3f8f4c67eac17207df1e887e0adf7d497dd6f3.tar.gz |
[infra-netif] select infrastructure link among multiple candidates (#1353)
This commit implements a general Infrastructure Link Selection that
allows otbr-agent to select one network interface among multiple
network interface candidates (specified by multiple `-B` arguments) as
the infrastructure link.
The Infrastructure Link Selection uses following rules:
- The network link in the most usable state is selected
- Prefer `up and running` to `up`
- Prefer `up` to `down`
- Prefer `down` to `invalid`
- Once an interface is selected, it's preferred if either is true:
- The interface is still `up and running`
- No other interface is `up and running`
- The interface has been `up and running` within last 10 seconds
`otbr-agent` uses Infrastructure Link Selection to select the correct
infrastructure link:
- Run Infrastructure Link Selection when started to select the
infrastructure link
- Run Infrastructure Link Selection when network interface states
change is detected. otbr-agent would quit if a different
infrastructure link is selected, expecting the system to restart it
to use the new infrastructure link.
Note:
- The Infrastructure Link Selection mechanism is effectively disabled
if less than two infrastructure link names are specified.
- The current implementation only works for Linux platforms.
-rw-r--r-- | Android.mk | 2 | ||||
-rw-r--r-- | etc/cmake/options.cmake | 7 | ||||
-rwxr-xr-x | script/test | 1 | ||||
-rw-r--r-- | src/agent/application.cpp | 28 | ||||
-rw-r--r-- | src/agent/application.hpp | 10 | ||||
-rw-r--r-- | src/agent/main.cpp | 25 | ||||
-rw-r--r-- | src/common/types.hpp | 27 | ||||
-rw-r--r-- | src/utils/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/utils/infra_link_selector.cpp | 353 | ||||
-rw-r--r-- | src/utils/infra_link_selector.hpp | 151 | ||||
-rw-r--r-- | src/utils/socket_utils.cpp | 32 | ||||
-rw-r--r-- | src/utils/socket_utils.hpp | 11 | ||||
-rwxr-xr-x | tests/scripts/infra-link-selector | 145 |
13 files changed, 762 insertions, 31 deletions
@@ -134,6 +134,8 @@ LOCAL_SRC_FILES := \ src/sdp_proxy/discovery_proxy.cpp \ src/utils/dns_utils.cpp \ src/utils/hex.cpp \ + src/utils/infra_link_selector.cpp \ + src/utils/socket_utils.cpp \ src/utils/string_utils.cpp \ src/utils/thread_helper.cpp \ diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake index 8cd95d42..a4d244d0 100644 --- a/etc/cmake/options.cmake +++ b/etc/cmake/options.cmake @@ -93,3 +93,10 @@ option(OTBR_NOTIFY_UPSTART "Notify upstart when ready." ON) if(OTBR_NOTIFY_UPSTART) target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_NOTIFY_UPSTART=1) endif() + +option(OTBR_VENDOR_INFRA_LINK_SELECT "Enable Vendor-specific infrastructure link selection rules" OFF) +if(OTBR_VENDOR_INFRA_LINK_SELECT) + target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_VENDOR_INFRA_LINK_SELECT=1) +else() + target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_VENDOR_INFRA_LINK_SELECT=0) +endif() diff --git a/script/test b/script/test index ccef590e..5a7b9113 100755 --- a/script/test +++ b/script/test @@ -182,6 +182,7 @@ do_simulation() export top_builddir="${OTBR_TOP_BUILDDIR}" print_result tests/scripts/auto-attach + print_result tests/scripts/infra-link-selector } main() diff --git a/src/agent/application.cpp b/src/agent/application.cpp index 1ddd4741..eed641e3 100644 --- a/src/agent/application.cpp +++ b/src/agent/application.cpp @@ -40,6 +40,7 @@ #include "agent/application.hpp" #include "common/code_utils.hpp" #include "common/mainloop_manager.hpp" +#include "utils/infra_link_selector.hpp" namespace otbr { @@ -47,17 +48,22 @@ std::atomic_bool Application::sShouldTerminate(false); const struct timeval Application::kPollTimeout = {10, 0}; Application::Application(const std::string & aInterfaceName, - const std::string & aBackboneInterfaceName, + const std::vector<const char *> &aBackboneInterfaceNames, const std::vector<const char *> &aRadioUrls, bool aEnableAutoAttach) : mInterfaceName(aInterfaceName) - , mBackboneInterfaceName(aBackboneInterfaceName) - , mNcp(mInterfaceName.c_str(), aRadioUrls, mBackboneInterfaceName.c_str(), /* aDryRun */ false, aEnableAutoAttach) +#if __linux__ + , mInfraLinkSelector(aBackboneInterfaceNames) + , mBackboneInterfaceName(mInfraLinkSelector.Select()) +#else + , mBackboneInterfaceName(aBackboneInterfaceNames.empty() ? "" : aBackboneInterfaceNames.front()) +#endif + , mNcp(mInterfaceName.c_str(), aRadioUrls, mBackboneInterfaceName, /* aDryRun */ false, aEnableAutoAttach) #if OTBR_ENABLE_BORDER_AGENT , mBorderAgent(mNcp) #endif #if OTBR_ENABLE_BACKBONE_ROUTER - , mBackboneAgent(mNcp, aInterfaceName, aBackboneInterfaceName) + , mBackboneAgent(mNcp, aInterfaceName, mBackboneInterfaceName) #endif #if OTBR_ENABLE_OPENWRT , mUbusAgent(mNcp) @@ -111,7 +117,7 @@ otbrError Application::Run(void) { otbrError error = OTBR_ERROR_NONE; - otbrLogInfo("Border router agent started."); + otbrLogInfo("Thread Border Router started on AIL %s.", mBackboneInterfaceName); #ifdef HAVE_LIBSYSTEMD if (getenv("SYSTEMD_EXEC_PID") != nullptr) @@ -158,6 +164,18 @@ otbrError Application::Run(void) if (rval >= 0) { MainloopManager::GetInstance().Process(mainloop); + +#if __linux__ + { + const char *newInfraLink = mInfraLinkSelector.Select(); + + if (mBackboneInterfaceName != newInfraLink) + { + error = OTBR_ERROR_INFRA_LINK_CHANGED; + break; + } + } +#endif } else if (errno != EINTR) { diff --git a/src/agent/application.hpp b/src/agent/application.hpp index 185b6a88..d12ed154 100644 --- a/src/agent/application.hpp +++ b/src/agent/application.hpp @@ -58,6 +58,7 @@ #if OTBR_ENABLE_VENDOR_SERVER #include "agent/vendor.hpp" #endif +#include "utils/infra_link_selector.hpp" namespace otbr { @@ -87,7 +88,7 @@ public: * */ explicit Application(const std::string & aInterfaceName, - const std::string & aBackboneInterfaceName, + const std::vector<const char *> &aBackboneInterfaceNames, const std::vector<const char *> &aRadioUrls, bool aEnableAutoAttach); @@ -118,8 +119,11 @@ private: static void HandleSignal(int aSignal); - std::string mInterfaceName; - std::string mBackboneInterfaceName; + std::string mInterfaceName; +#if __linux__ + otbr::Utils::InfraLinkSelector mInfraLinkSelector; +#endif + const char * mBackboneInterfaceName; Ncp::ControllerOpenThread mNcp; #if OTBR_ENABLE_BORDER_AGENT BorderAgent mBorderAgent; diff --git a/src/agent/main.cpp b/src/agent/main.cpp index 39a8a8d6..14c33cba 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -37,13 +37,13 @@ #include <string> #include <vector> +#include <assert.h> #include <getopt.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> -#include <assert.h> #include <openthread/logging.h> #include <openthread/platform/radio.h> @@ -184,13 +184,13 @@ static int realmain(int argc, char *argv[]) { otbrLogLevel logLevel = GetDefaultLogLevel(); int opt; - int ret = EXIT_SUCCESS; - const char * interfaceName = kDefaultInterfaceName; - const char * backboneInterfaceName = ""; - bool verbose = false; - bool printRadioVersion = false; - bool enableAutoAttach = true; + int ret = EXIT_SUCCESS; + const char * interfaceName = kDefaultInterfaceName; + bool verbose = false; + bool printRadioVersion = false; + bool enableAutoAttach = true; std::vector<const char *> radioUrls; + std::vector<const char *> backboneInterfaceNames; long parseResult; std::set_new_handler(OnAllocateFailed); @@ -200,7 +200,8 @@ static int realmain(int argc, char *argv[]) switch (opt) { case OTBR_OPT_BACKBONE_INTERFACE_NAME: - backboneInterfaceName = optarg; + backboneInterfaceNames.push_back(optarg); + otbrLogNotice("Backbone interface: %s", optarg); break; case OTBR_OPT_DEBUG_LEVEL: @@ -254,7 +255,11 @@ static int realmain(int argc, char *argv[]) otbrLogNotice("Running %s", OTBR_PACKAGE_VERSION); otbrLogNotice("Thread version: %s", otbr::Ncp::ControllerOpenThread::GetThreadVersion()); otbrLogNotice("Thread interface: %s", interfaceName); - otbrLogNotice("Backbone interface: %s", backboneInterfaceName); + + if (backboneInterfaceNames.empty()) + { + otbrLogNotice("Backbone interface is not specified"); + } for (int i = optind; i < argc; i++) { @@ -269,7 +274,7 @@ static int realmain(int argc, char *argv[]) } { - otbr::Application app(interfaceName, backboneInterfaceName, radioUrls, enableAutoAttach); + otbr::Application app(interfaceName, backboneInterfaceNames, radioUrls, enableAutoAttach); gApp = &app; app.Init(); diff --git a/src/common/types.hpp b/src/common/types.hpp index 80eaa51f..6035c1cb 100644 --- a/src/common/types.hpp +++ b/src/common/types.hpp @@ -71,19 +71,20 @@ enum otbrError { OTBR_ERROR_NONE = 0, ///< No error. - OTBR_ERROR_ERRNO = -1, ///< Error defined by errno. - OTBR_ERROR_DBUS = -2, ///< DBus error. - OTBR_ERROR_MDNS = -3, ///< mDNS error. - OTBR_ERROR_OPENTHREAD = -4, ///< OpenThread error. - OTBR_ERROR_REST = -5, ///< Rest Server error. - OTBR_ERROR_SMCROUTE = -6, ///< SMCRoute error. - OTBR_ERROR_NOT_FOUND = -7, ///< Not found. - OTBR_ERROR_PARSE = -8, ///< Parse error. - OTBR_ERROR_NOT_IMPLEMENTED = -9, ///< Not implemented error. - OTBR_ERROR_INVALID_ARGS = -10, ///< Invalid arguments error. - OTBR_ERROR_DUPLICATED = -11, ///< Duplicated operation, resource or name. - OTBR_ERROR_ABORTED = -12, ///< The operation is aborted. - OTBR_ERROR_INVALID_STATE = -13, ///< The target isn't in a valid state. + OTBR_ERROR_ERRNO = -1, ///< Error defined by errno. + OTBR_ERROR_DBUS = -2, ///< DBus error. + OTBR_ERROR_MDNS = -3, ///< mDNS error. + OTBR_ERROR_OPENTHREAD = -4, ///< OpenThread error. + OTBR_ERROR_REST = -5, ///< Rest Server error. + OTBR_ERROR_SMCROUTE = -6, ///< SMCRoute error. + OTBR_ERROR_NOT_FOUND = -7, ///< Not found. + OTBR_ERROR_PARSE = -8, ///< Parse error. + OTBR_ERROR_NOT_IMPLEMENTED = -9, ///< Not implemented error. + OTBR_ERROR_INVALID_ARGS = -10, ///< Invalid arguments error. + OTBR_ERROR_DUPLICATED = -11, ///< Duplicated operation, resource or name. + OTBR_ERROR_ABORTED = -12, ///< The operation is aborted. + OTBR_ERROR_INVALID_STATE = -13, ///< The target isn't in a valid state. + OTBR_ERROR_INFRA_LINK_CHANGED = -14, ///< The infrastructure link is changed. }; namespace otbr { diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index 26fb33b3..5a624844 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -30,6 +30,7 @@ add_library(otbr-utils crc16.cpp dns_utils.cpp hex.cpp + infra_link_selector.cpp pskc.cpp socket_utils.cpp steering_data.cpp diff --git a/src/utils/infra_link_selector.cpp b/src/utils/infra_link_selector.cpp new file mode 100644 index 00000000..ee68ec3d --- /dev/null +++ b/src/utils/infra_link_selector.cpp @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2022, 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. + */ + +/** + * @file + * The file implements the Infrastructure Link Selector. + */ + +#if __linux__ + +#define OTBR_LOG_TAG "ILS" + +#include "utils/infra_link_selector.hpp" + +#include <linux/rtnetlink.h> +#include <net/if.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <unistd.h> + +#include <openthread/backbone_router_ftd.h> + +#include "utils/socket_utils.hpp" + +namespace otbr { +namespace Utils { + +constexpr Milliseconds InfraLinkSelector::kInfraLinkSelectionDelay; + +bool InfraLinkSelector::LinkInfo::Update(LinkState aState) +{ + bool changed = mState != aState; + + VerifyOrExit(changed); + + if (mState == kUpAndRunning && aState != kUpAndRunning) + { + mWasUpAndRunning = true; + mLastRunningTime = Clock::now(); + } + + mState = aState; +exit: + return changed; +} + +InfraLinkSelector::InfraLinkSelector(std::vector<const char *> aInfraLinkNames) + : mInfraLinkNames(std::move(aInfraLinkNames)) +{ + if (mInfraLinkNames.size() >= 2) + { + mNetlinkSocket = CreateNetLinkRouteSocket(RTMGRP_LINK); + VerifyOrDie(mNetlinkSocket != -1, "Failed to create netlink socket"); + } + + for (const char *name : mInfraLinkNames) + { + mInfraLinkInfos[name].Update(QueryInfraLinkState(name)); + } +} + +InfraLinkSelector::~InfraLinkSelector(void) +{ + if (mNetlinkSocket != -1) + { + close(mNetlinkSocket); + mNetlinkSocket = -1; + } +} + +const char *InfraLinkSelector::Select(void) +{ + const char *sel; + +#if OTBR_ENABLE_VENDOR_INFRA_LINK_SELECT + sel = otbrVendorInfraLinkSelect(); + + if (sel == nullptr) + { + sel = SelectGeneric(); + } +#else + sel = SelectGeneric(); +#endif + + return sel; +} + +const char *InfraLinkSelector::SelectGeneric(void) +{ + const char * prevInfraLink = mCurrentInfraLink; + InfraLinkSelector::LinkState currentInfraLinkState = kInvalid; + auto now = Clock::now(); + LinkInfo * currentInfraLinkInfo = nullptr; + + VerifyOrExit(!mInfraLinkNames.empty(), mCurrentInfraLink = kDefaultInfraLinkName); + VerifyOrExit(mInfraLinkNames.size() > 1, mCurrentInfraLink = mInfraLinkNames.front()); + VerifyOrExit(mRequireReselect, assert(mCurrentInfraLink != nullptr)); + + otbrLogInfo("Evaluating infra link among %zu netifs:", mInfraLinkNames.size()); + + // Prefer `mCurrentInfraLink` if it's up and running. + if (mCurrentInfraLink != nullptr) + { + currentInfraLinkInfo = &mInfraLinkInfos[mCurrentInfraLink]; + + otbrLogInfo("\tInfra link %s is in state %s", mCurrentInfraLink, + LinkStateToString(currentInfraLinkInfo->mState)); + + VerifyOrExit(currentInfraLinkInfo->mState != kUpAndRunning); + } + + // Select an infra link with best state. + { + const char * bestInfraLink = mCurrentInfraLink; + InfraLinkSelector::LinkState bestState = currentInfraLinkState; + + for (const char *name : mInfraLinkNames) + { + if (name != mCurrentInfraLink) + { + LinkInfo &linkInfo = mInfraLinkInfos[name]; + + otbrLogInfo("\tInfra link %s is in state %s", name, LinkStateToString(linkInfo.mState)); + if (bestInfraLink == nullptr || linkInfo.mState > bestState) + { + bestInfraLink = name; + bestState = linkInfo.mState; + } + } + } + + VerifyOrExit(bestInfraLink != mCurrentInfraLink); + + // Prefer `mCurrentInfraLink` if no other infra link is up and running + VerifyOrExit(mCurrentInfraLink == nullptr || bestState == kUpAndRunning); + + // Prefer `mCurrentInfraLink` if it's down for less than `kInfraLinkSelectionDelay` + if (mCurrentInfraLink != nullptr && currentInfraLinkInfo->mWasUpAndRunning) + { + Milliseconds timeSinceLastRunning = + std::chrono::duration_cast<Milliseconds>(now - currentInfraLinkInfo->mLastRunningTime); + + if (timeSinceLastRunning < kInfraLinkSelectionDelay) + { + Milliseconds delay = kInfraLinkSelectionDelay - timeSinceLastRunning; + + otbrLogInfo("Infra link %s was running %lldms ago, wait for %lldms to recheck.", mCurrentInfraLink, + timeSinceLastRunning.count(), delay.count()); + mTaskRunner.Post(delay, [this]() { mRequireReselect = true; }); + ExitNow(); + } + } + + // Current infra link changed. + mCurrentInfraLink = bestInfraLink; + } + +exit: + if (mRequireReselect) + { + mRequireReselect = false; + + if (prevInfraLink != mCurrentInfraLink) + { + if (prevInfraLink == nullptr) + { + otbrLogNotice("Infra link selected: %s", mCurrentInfraLink); + } + else + { + otbrLogWarning("Infra link switched from %s to %s", prevInfraLink, mCurrentInfraLink); + } + } + else + { + otbrLogInfo("Infra link unchanged: %s", mCurrentInfraLink); + } + } + + return mCurrentInfraLink; +} + +InfraLinkSelector::LinkState InfraLinkSelector::QueryInfraLinkState(const char *aInfraLinkName) +{ + int sock = 0; + struct ifreq ifReq; + InfraLinkSelector::LinkState state = kInvalid; + + sock = SocketWithCloseExec(AF_INET6, SOCK_DGRAM, IPPROTO_IP, kSocketBlock); + VerifyOrDie(sock != -1, "Failed to create AF_INET6 socket."); + + memset(&ifReq, 0, sizeof(ifReq)); + strncpy(ifReq.ifr_name, aInfraLinkName, sizeof(ifReq.ifr_name) - 1); + + VerifyOrExit(ioctl(sock, SIOCGIFFLAGS, &ifReq) != -1); + + state = (ifReq.ifr_flags & IFF_UP) ? ((ifReq.ifr_flags & IFF_RUNNING) ? kUpAndRunning : kUp) : kDown; + +exit: + if (sock != 0) + { + close(sock); + } + + return state; +} + +void InfraLinkSelector::Update(MainloopContext &aMainloop) +{ + if (mNetlinkSocket != -1) + { + FD_SET(mNetlinkSocket, &aMainloop.mReadFdSet); + aMainloop.mMaxFd = std::max(mNetlinkSocket, aMainloop.mMaxFd); + } +} + +void InfraLinkSelector::Process(const MainloopContext &aMainloop) +{ + OTBR_UNUSED_VARIABLE(aMainloop); + + if (mNetlinkSocket != -1 && FD_ISSET(mNetlinkSocket, &aMainloop.mReadFdSet)) + { + ReceiveNetLinkMessage(); + } +} + +void InfraLinkSelector::ReceiveNetLinkMessage(void) +{ + const size_t kMaxNetLinkBufSize = 8192; + ssize_t len; + union + { + nlmsghdr mHeader; + uint8_t mBuffer[kMaxNetLinkBufSize]; + } msgBuffer; + + len = recv(mNetlinkSocket, msgBuffer.mBuffer, sizeof(msgBuffer.mBuffer), 0); + if (len < 0) + { + otbrLogWarning("Failed to receive netlink message: %s", strerror(errno)); + ExitNow(); + } + + for (struct nlmsghdr *header = &msgBuffer.mHeader; NLMSG_OK(header, static_cast<size_t>(len)); + header = NLMSG_NEXT(header, len)) + { + struct ifinfomsg *ifinfo; + + switch (header->nlmsg_type) + { + case RTM_NEWLINK: + case RTM_DELLINK: + ifinfo = reinterpret_cast<struct ifinfomsg *>(NLMSG_DATA(header)); + HandleInfraLinkStateChange(ifinfo->ifi_index); + break; + case NLMSG_ERROR: + { + struct nlmsgerr *errMsg = reinterpret_cast<struct nlmsgerr *>(NLMSG_DATA(header)); + + otbrLogWarning("netlink NLMSG_ERROR response: seq=%u, error=%d", header->nlmsg_seq, errMsg->error); + break; + } + default: + break; + } + } + +exit: + return; +} + +void InfraLinkSelector::HandleInfraLinkStateChange(uint32_t aInfraLinkIndex) +{ + const char *infraLinkName = nullptr; + + for (const char *name : mInfraLinkNames) + { + if (aInfraLinkIndex == if_nametoindex(name)) + { + infraLinkName = name; + break; + } + } + + VerifyOrExit(infraLinkName != nullptr); + + { + LinkInfo &linkInfo = mInfraLinkInfos[infraLinkName]; + LinkState prevState = linkInfo.mState; + + if (linkInfo.Update(QueryInfraLinkState(infraLinkName))) + { + otbrLogInfo("Infra link name %s index %lu state changed: %s -> %s", infraLinkName, aInfraLinkIndex, + LinkStateToString(prevState), LinkStateToString(linkInfo.mState)); + mRequireReselect = true; + } + } +exit: + return; +} + +const char *InfraLinkSelector::LinkStateToString(LinkState aState) +{ + const char *str = ""; + + switch (aState) + { + case kInvalid: + str = "INVALID"; + break; + case kDown: + str = "DOWN"; + break; + case kUp: + str = "UP"; + break; + case kUpAndRunning: + str = "UP+RUNNING"; + break; + } + return str; +} + +} // namespace Utils +} // namespace otbr + +#endif // __linux__ diff --git a/src/utils/infra_link_selector.hpp b/src/utils/infra_link_selector.hpp new file mode 100644 index 00000000..250648e3 --- /dev/null +++ b/src/utils/infra_link_selector.hpp @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2022, 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. + */ + +/** + * @file + * This file include definitions for the Infrastructure Link Selector. + */ + +#ifndef INFRA_LINK_SELECTOR_HPP_ +#define INFRA_LINK_SELECTOR_HPP_ + +#if __linux__ + +#include <assert.h> +#include <map> +#include <utility> +#include <vector> + +#include <openthread/backbone_router_ftd.h> + +#include "common/code_utils.hpp" +#include "common/mainloop.hpp" +#include "common/task_runner.hpp" +#include "common/time.hpp" + +#if OTBR_ENABLE_VENDOR_INFRA_LINK_SELECT +/** + * This function implements platform specific rules for selecting infrastructure link. + * + * This function should return the infrastructure link that is selected by platform specific rules. + * If the function returns nullptr, the generic infrastructure link selections rules will be applied. + * + */ +extern "C" const char *otbrVendorInfraLinkSelect(void); +#endif + +namespace otbr { +namespace Utils { + +/** + * This class implements Infrastructure Link Selector. + * + */ +class InfraLinkSelector : public MainloopProcessor, private NonCopyable +{ +public: + /** + * This constructor initializes the InfraLinkSelector instance. + * + * @param[in] aInfraLinkNames A list of infrastructure link candidates to select from. + * + */ + explicit InfraLinkSelector(std::vector<const char *> aInfraLinkNames); + + /** + * This destructor destroys the InfraLinkSelector instance. + * + */ + ~InfraLinkSelector(void); + + /** + * This method selects an infrastructure link among infrastructure link candidates using rules below: + * + * The infrastructure link in the most usable state is selected: + * Prefer `up and running` to `up` + * Prefer `up` to `down` + * Prefer `down` to `invalid` + * Once an interface is selected, it's preferred if either is true: + * The interface is still `up and running` + * No other interface is `up and running` + * The interface has been `up and running` within last 10 seconds + * + * @returns The selected infrastructure link. + * + */ + const char *Select(void); + +private: + /** + * This enumeration infrastructure link states. + * + */ + enum LinkState : uint8_t + { + kInvalid, ///< The infrastructure link is invalid. + kDown, ///< The infrastructure link is down. + kUp, ///< The infrastructure link is up, but not running. + kUpAndRunning, ///< The infrastructure link is up and running. + + }; + + struct LinkInfo + { + LinkState mState = kInvalid; + Clock::time_point mLastRunningTime; + bool mWasUpAndRunning = false; + + bool Update(LinkState aState); + }; + + static constexpr const char *kDefaultInfraLinkName = ""; + static constexpr auto kInfraLinkSelectionDelay = Milliseconds(10000); + + const char *SelectGeneric(void); + + static const char *LinkStateToString(LinkState aState); + static LinkState QueryInfraLinkState(const char *aInfraLinkName); + void Update(MainloopContext &aMainloop) override; + void Process(const MainloopContext &aMainloop) override; + void ReceiveNetLinkMessage(void); + void HandleInfraLinkStateChange(uint32_t aInfraLinkIndex); + + std::vector<const char *> mInfraLinkNames; + std::map<const char *, LinkInfo> mInfraLinkInfos; + int mNetlinkSocket = -1; + const char * mCurrentInfraLink = nullptr; + TaskRunner mTaskRunner; + bool mRequireReselect = true; +}; + +} // namespace Utils +} // namespace otbr + +#endif // __linux__ + +#endif // INFRA_LINK_SELECTOR_HPP_ diff --git a/src/utils/socket_utils.cpp b/src/utils/socket_utils.cpp index 54b806c3..0bc1e606 100644 --- a/src/utils/socket_utils.cpp +++ b/src/utils/socket_utils.cpp @@ -28,6 +28,11 @@ #include "utils/socket_utils.hpp" +#if __linux__ +#include <linux/netlink.h> +#include <linux/rtnetlink.h> +#endif + #include "common/code_utils.hpp" int SocketWithCloseExec(int aDomain, int aType, int aProtocol, SocketBlockOption aBlockOption) @@ -55,3 +60,30 @@ exit: return fd; } + +#if __linux__ +int CreateNetLinkRouteSocket(uint32_t aNlGroups) +{ + int sock; + int rval = 0; + struct sockaddr_nl addr; + + sock = SocketWithCloseExec(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE, kSocketBlock); + VerifyOrExit(sock != -1); + + memset(&addr, 0, sizeof(addr)); + addr.nl_family = AF_NETLINK; + addr.nl_groups = aNlGroups; // e.g. RTMGRP_LINK | RTMGRP_IPV6_IFADDR; + + rval = bind(sock, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)); + +exit: + if (rval != 0) + { + close(sock); + sock = -1; + } + + return sock; +} +#endif diff --git a/src/utils/socket_utils.hpp b/src/utils/socket_utils.hpp index b135c927..33986d3e 100644 --- a/src/utils/socket_utils.hpp +++ b/src/utils/socket_utils.hpp @@ -58,4 +58,15 @@ enum SocketBlockOption */ int SocketWithCloseExec(int aDomain, int aType, int aProtocol, SocketBlockOption aBlockOption); +/** + * This function creates a Linux netlink NETLINK_ROUTE socket for receiving routing and link updates. + * + * @param[in] aNlGroups The netlink multicast groups to listen to. + * + * @retval -1 Failed to create the netlink socket. + * @retval ... The file descriptor of the created netlink socket. + * + */ +int CreateNetLinkRouteSocket(uint32_t aNlGroups); + #endif // OTBR_UTILS_SOCKET_UTILS_HPP_ diff --git a/tests/scripts/infra-link-selector b/tests/scripts/infra-link-selector new file mode 100755 index 00000000..aa3c8a0c --- /dev/null +++ b/tests/scripts/infra-link-selector @@ -0,0 +1,145 @@ +#!/bin/bash +# +# Copyright (c) 2022, 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. + +set -euxo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +readonly ABS_TOP_BUILDDIR="$(cd "${top_builddir:-"${SCRIPT_DIR}"/../../}" && pwd)" +readonly OTBR_AGENT="${ABS_TOP_BUILDDIR}/src/agent/otbr-agent" +readonly OT_RCP=$(command -v ot-rcp) + +at_exit() +{ + EXIT_CODE=$? + + sudo killall otbr-agent || true + + sudo ip link del ilstest0 || true + sudo ip link del ilstest1 || true + sudo ip link del ilstest2 || true + + exit $EXIT_CODE +} + +trap at_exit INT TERM EXIT + +sudo cp "${ABS_TOP_BUILDDIR}/src/agent/otbr-agent.conf" /etc/dbus-1/system.d/ +sudo chmod +r /etc/dbus-1/system.d/otbr-agent.conf +sudo systemctl reload dbus + +sudo modprobe dummy + +prepare_infra_link() +{ + local netif="$1" + local mac="$2" + + sudo ip link add "${netif}" type dummy + sudo ifconfig "${netif}" hw ether "${mac}" + sudo ifconfig "${netif}" up +} + +sudo ip link del ilstest0 || true +sudo ip link del ilstest1 || true +sudo ip link del ilstest2 || true + +prepare_infra_link "ilstest0" "C8:D7:4A:4E:47:00" +prepare_infra_link "ilstest1" "C8:D7:4A:4E:47:01" +prepare_infra_link "ilstest2" "C8:D7:4A:4E:47:02" + +sleep 10 +ifconfig +ip link list + +sudo "${OTBR_AGENT}" -I wpan0 -v -d7 -B ilstest0 -B ilstest1 -B ilstest2 "spinel+hdlc+forkpty://${OT_RCP}?forkpty-arg=1" 2>&1 | tee output & + +function check_infra_link() +{ + grep "\-ILS\-\-\-\-\-: Infra link \(selected\|unchanged\|switched\)" output | tail -1 +} + +function verify_otbr_agent_exited() +{ + if pgrep otbr-agent; then + return 1 + fi +} + +sleep 3 +# Verify ILS selects ilstest0 +check_infra_link | grep "selected: ilstest0" + +sudo ifconfig ilstest1 down +sudo ifconfig ilstest2 down + +sleep 3 +# Verify ILS keeps using ilstest0 +check_infra_link | grep "unchanged: ilstest0" + +sudo ifconfig ilstest2 up + +sleep 3 +# Verify ILS keeps using ilstest0 because ilstest0 is still RUNNING +check_infra_link | grep "unchanged: ilstest0" + +sudo ifconfig ilstest0 down +sleep 3 +# Verify ILS keeps using ilstest0 because ilstest0 was RUNNING recently +check_infra_link | grep "unchanged: ilstest0" + +sudo ifconfig ilstest0 up +sleep 11 + +# Verify ILS keeps using ilstest0 because ilstest0 is RUNNING again +check_infra_link | grep "unchanged: ilstest0" + +sudo ifconfig ilstest0 down +sleep 11 +# Verify ILS switches to ilstest2 after ilstest0 is DOWN for more than 10s +check_infra_link | grep "switched from ilstest0 to ilstest2" +verify_otbr_agent_exited + +# Now, only ilstest2 is RUNNING + +sudo "${OTBR_AGENT}" -I wpan0 -v -d7 -B ilstest0 -B ilstest1 -B ilstest2 "spinel+hdlc+forkpty://${OT_RCP}?forkpty-arg=1" 2>&1 | tee output & + +sleep 3 +# Verify ILS selects ilstest2 after reboot +check_infra_link | grep "selected: ilstest2" + +sudo ifconfig ilstest2 down +sleep 3 +# Verify ILS keeps using ilstest2 because ilstest2 was RUNNING recently +check_infra_link | grep "unchanged: ilstest2" + +sleep 8 +sudo ifconfig ilstest1 up +sleep 3 +# Verify ILS switches to ilstest1 because ilstest2 was not RUNNING for more than 10s +check_infra_link | grep "switched from ilstest2 to ilstest1" +verify_otbr_agent_exited |