aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Lin <simonlin@google.com>2022-07-27 11:34:07 +0800
committerGitHub <noreply@github.com>2022-07-26 20:34:07 -0700
commitfe3f8f4c67eac17207df1e887e0adf7d497dd6f3 (patch)
treee6b2724218f3a5de3359cccb74bb92af4653049c
parentc70d2a03536366fddf4babdbf4cd6ad7a315d151 (diff)
downloadot-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.mk2
-rw-r--r--etc/cmake/options.cmake7
-rwxr-xr-xscript/test1
-rw-r--r--src/agent/application.cpp28
-rw-r--r--src/agent/application.hpp10
-rw-r--r--src/agent/main.cpp25
-rw-r--r--src/common/types.hpp27
-rw-r--r--src/utils/CMakeLists.txt1
-rw-r--r--src/utils/infra_link_selector.cpp353
-rw-r--r--src/utils/infra_link_selector.hpp151
-rw-r--r--src/utils/socket_utils.cpp32
-rw-r--r--src/utils/socket_utils.hpp11
-rwxr-xr-xtests/scripts/infra-link-selector145
13 files changed, 762 insertions, 31 deletions
diff --git a/Android.mk b/Android.mk
index 0cec3e5f..cf783373 100644
--- a/Android.mk
+++ b/Android.mk
@@ -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