diff --git a/.docker/netremote-dev/Dockerfile b/.docker/netremote-dev/Dockerfile index 04f5ceb6..be0fcb6e 100644 --- a/.docker/netremote-dev/Dockerfile +++ b/.docker/netremote-dev/Dockerfile @@ -22,7 +22,7 @@ LABEL org.label-schema.schema-version = "1.0" # sudo apt-get update # # 2. Install core build tools and dependencies: -# sudo apt-get install -y --no-install-recommends build-essential ca-certificates cmake curl dotnet7 git gnupg linux-libc-dev ninja-build pkg-config tar unzip zip libnl-3-dev libssl-dev libnl-genl-3-dev libdbus-c++-dev libnl-route-3-dev +# sudo apt-get install -y --no-install-recommends build-essential ca-certificates cmake curl dotnet7 git gnupg linux-libc-dev ninja-build pkg-config tar unzip zip libnl-3-200-dbg libnl-3-dev libssl-dev libnl-genl-3-dev libdbus-c++-dev libnl-route-3-dev # # 3. Remove llvm 16 toolchain packages to avoid conflicts with llvm 17 toolchain. # sudo apt-get remove -y --purge clang-16* lldb-16* llvm-16* @@ -57,7 +57,8 @@ RUN apt-get update && \ unzip \ zip \ # hostapd build dependencies. - # libnl-3-dev libssl-dev libnl-genl-3-dev + # libnl-3-200-dbg libnl-3-dev libssl-dev libnl-genl-3-dev + libnl-3-200-dbg \ libnl-3-dev \ libnl-genl-3-dev \ libssl-dev \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 2df35e24..5791bbb7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,8 +55,8 @@ include(GNUInstallDirs) include(grpc) include(protoc) -# Enable verbose output until project is bootstrapped. -set(CMAKE_VERBOSE_MAKEFILE ON) +# Enable to debug CMake issues. +set(CMAKE_VERBOSE_MAKEFILE OFF CACHE BOOL "Verbose Makefile Generation") # Pull in external dependencies. # Look for protobuf-config.cmake. diff --git a/CMakePresets.json b/CMakePresets.json index 8c0a8b0c..0696455b 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -45,7 +45,7 @@ "cacheVariables": { "CMAKE_CXX_COMPILER": "/usr/bin/clang++-17", "CMAKE_C_COMPILER": "/usr/bin/clang-17", - "CMAKE_GENERATOR": "Ninja" + "CMAKE_GENERATOR": "Ninja Multi-Config" } }, { @@ -106,10 +106,7 @@ "os-linux", "linux-base", "release-base" - ], - "cacheVariables": { - "CMAKE_GENERATOR": "Ninja Multi-Config" - } + ] }, { "name": "release-windows", @@ -123,7 +120,8 @@ "buildPresets": [ { "name": "dev-linux", - "configurePreset": "dev-linux" + "configurePreset": "dev-linux", + "configuration": "Debug" }, { "name": "dev-windows", diff --git a/src/common/shared/notstd/CMakeLists.txt b/src/common/shared/notstd/CMakeLists.txt index 0c41e35d..04071f3c 100644 --- a/src/common/shared/notstd/CMakeLists.txt +++ b/src/common/shared/notstd/CMakeLists.txt @@ -12,6 +12,7 @@ target_sources(notstd FILES ${NOTSTD_PUBLIC_INCLUDE_PREFIX}/Exceptions.hxx ${NOTSTD_PUBLIC_INCLUDE_PREFIX}/Memory.hxx + ${NOTSTD_PUBLIC_INCLUDE_PREFIX}/Scope.hxx ${NOTSTD_PUBLIC_INCLUDE_PREFIX}/Utility.hxx ) diff --git a/src/common/shared/notstd/include/notstd/Scope.hxx b/src/common/shared/notstd/include/notstd/Scope.hxx new file mode 100644 index 00000000..a5bfef0b --- /dev/null +++ b/src/common/shared/notstd/include/notstd/Scope.hxx @@ -0,0 +1,129 @@ +#ifndef NOT_STD_SCOPE_HXX +#define NOT_STD_SCOPE_HXX + +#include +#include + +namespace notstd +{ +namespace details +{ +/** + * @brief Executes a given lambda when destroyed. + * + * @tparam TLambda + */ +template +class lambda_call +{ +public: + lambda_call(const lambda_call&) = delete; + lambda_call& + operator=(const lambda_call&) = delete; + lambda_call& + operator=(lambda_call&& other) = delete; + + /** + * @brief Construct a new lambda call object. + * + * @param lambda + */ + explicit lambda_call(TLambda&& lambda) noexcept : + m_lambda(std::move(lambda)) + { + static_assert(std::is_same::value, "scope_exit lambdas must not have a return value"); + static_assert(!std::is_lvalue_reference::value && !std::is_rvalue_reference::value, + "scope_exit should only be directly used with a lambda"); + } + + /** + * @brief Construct a new lambda call object. + * + * @param other + */ + lambda_call(lambda_call&& other) noexcept : + m_lambda(std::move(other.m_lambda)), + m_call(other.m_call) + { + other.m_call = false; + } + + /** + * @brief Destroy the lambda call object. + */ + ~lambda_call() noexcept + { + reset(); + } + + /** + * @brief Ensures the scope_exit lambda will not be called. + */ + void + release() noexcept + { + m_call = false; + } + + /** + * @brief Executes the scope_exit lambda immediately if not yet run; ensures + * it will not run again. + */ + void + reset() noexcept + { + if (m_call) { + m_call = false; + m_lambda(); + } + } + + /** + * @brief Returns true if the scope_exit lambda is still going to be + * executed. + * + * @return true + * @return false + */ + explicit operator bool() const noexcept + { + return m_call; + } + +private: + TLambda m_lambda; + bool m_call = true; +}; +} // namespace details + +/** + * @brief Returns an object that executes the given lambda when destroyed. + * + * @tparam TLambda + * @param lambda + * @return auto + */ +template +[[nodiscard]] inline auto +scope_exit(TLambda&& lambda) noexcept +{ + return details::lambda_call(std::forward(lambda)); +} + +/** + * @brief Name alias for scope_exit. + * + * @tparam TLambda + * @param lambda + * @return auto + */ +template +[[nodiscard]] inline auto +ScopeExit(TLambda&& lambda) noexcept +{ + return scope_exit(std::forward(lambda)); +} + +} // namespace notstd + +#endif // NOT_STD_SCOPE_HXX diff --git a/src/linux/CMakeLists.txt b/src/linux/CMakeLists.txt index 35b9c49c..a0021781 100644 --- a/src/linux/CMakeLists.txt +++ b/src/linux/CMakeLists.txt @@ -1,5 +1,7 @@ add_subdirectory(external) +add_subdirectory(libnl-helpers) add_subdirectory(server) +add_subdirectory(tools) add_subdirectory(wifi) add_subdirectory(wpa-controller) diff --git a/src/linux/external/CMakeLists.txt b/src/linux/external/CMakeLists.txt index ad3d10ce..b76d104c 100644 --- a/src/linux/external/CMakeLists.txt +++ b/src/linux/external/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(hostap) +add_subdirectory(libnl) diff --git a/src/linux/external/libnl/CMakeLists.txt b/src/linux/external/libnl/CMakeLists.txt new file mode 100644 index 00000000..8e87f630 --- /dev/null +++ b/src/linux/external/libnl/CMakeLists.txt @@ -0,0 +1,29 @@ + +# Requires that libnl development package is installed (libnl-3-dev on ubuntu). +find_path(LIBNL_INCLUDE_DIR netlink/netlink.h + PATHS + /usr/include/libnl3 + /usr/local/include/libnl3 + REQUIRED +) + +# libnl core +find_library(LIBNL_STATIC NAMES libnl-3.a REQUIRED) +add_library(nl STATIC IMPORTED GLOBAL) +set_target_properties(nl PROPERTIES IMPORTED_LOCATION ${LIBNL_STATIC}) +set_target_properties(nl PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${LIBNL_INCLUDE_DIR}) +target_include_directories(nl INTERFACE ${LIBNL_INCLUDE_DIR}) + +# libnl-genl +find_library(LIBNL_GENL_STATIC NAMES libnl-genl-3.a REQUIRED) +add_library(nl-genl STATIC IMPORTED GLOBAL) +set_target_properties(nl-genl PROPERTIES IMPORTED_LOCATION ${LIBNL_GENL_STATIC}) +set_target_properties(nl-genl PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${LIBNL_INCLUDE_DIR}) +target_include_directories(nl-genl INTERFACE ${LIBNL_INCLUDE_DIR}) + +# libnl-route +find_library(LIBNL_ROUTE_STATIC NAMES libnl-route-3.a REQUIRED) +add_library(nl-route STATIC IMPORTED GLOBAL) +set_target_properties(nl-route PROPERTIES IMPORTED_LOCATION ${LIBNL_ROUTE_STATIC}) +set_target_properties(nl-route PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${LIBNL_INCLUDE_DIR}) +target_include_directories(nl-route INTERFACE ${LIBNL_INCLUDE_DIR}) diff --git a/src/linux/libnl-helpers/CMakeLists.txt b/src/linux/libnl-helpers/CMakeLists.txt new file mode 100644 index 00000000..3bdf79f4 --- /dev/null +++ b/src/linux/libnl-helpers/CMakeLists.txt @@ -0,0 +1,30 @@ + +add_library(libnl-helpers STATIC "") + +set(LIBNL_HELPERS_PUBLIC_INCLUDE ${CMAKE_CURRENT_LIST_DIR}/include) +set(LIBNL_HELPERS_PUBLIC_INCLUDE_SUFFIX microsoft/net/netlink) +set(LIBNL_HELPERS_PUBLIC_INCLUDE_PREFIX ${LIBNL_HELPERS_PUBLIC_INCLUDE}/${LIBNL_HELPERS_PUBLIC_INCLUDE_SUFFIX}) + +target_sources(libnl-helpers + PRIVATE + NetlinkSocket.cxx + NetlinkMessage.cxx + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${LIBNL_HELPERS_PUBLIC_INCLUDE} + FILES + ${LIBNL_HELPERS_PUBLIC_INCLUDE_PREFIX}/NetlinkMessage.hxx + ${LIBNL_HELPERS_PUBLIC_INCLUDE_PREFIX}/NetlinkSocket.hxx +) + +target_link_libraries(libnl-helpers + PUBLIC + nl +) + +install( + TARGETS libnl-helpers + EXPORT ${PROJECT_NAME} + FILE_SET HEADERS + PUBLIC_HEADER DESTINATION "${NETREMOTE_DIR_INSTALL_PUBLIC_HEADER_BASE}/${LIBNL_HELPERS_PUBLIC_INCLUDE_SUFFIX}" +) diff --git a/src/linux/libnl-helpers/NetlinkMessage.cxx b/src/linux/libnl-helpers/NetlinkMessage.cxx new file mode 100644 index 00000000..24795481 --- /dev/null +++ b/src/linux/libnl-helpers/NetlinkMessage.cxx @@ -0,0 +1,60 @@ + +#include + +using namespace Microsoft::Net::Netlink; + +NetlinkMessage::NetlinkMessage(struct nl_msg* message) : + Message(message) +{ +} + +NetlinkMessage::NetlinkMessage(NetlinkMessage&& other) : + Message(other.Message) +{ + other.Message = nullptr; +} + +NetlinkMessage& +NetlinkMessage::operator=(NetlinkMessage&& other) +{ + if (this != &other) { + Reset(); + Message = other.Message; + other.Message = nullptr; + } + + return *this; +} + +NetlinkMessage::~NetlinkMessage() +{ + Reset(); +} + +void +NetlinkMessage::Reset() +{ + if (Message != nullptr) { + nlmsg_free(Message); + Message = nullptr; + } +} + +struct nl_msg* +NetlinkMessage::Release() noexcept +{ + auto message = Message; + Message = nullptr; + return message; +} + +NetlinkMessage::operator struct nl_msg *() const noexcept +{ + return Message; +} + +struct nlmsghdr* +NetlinkMessage::Header() const noexcept +{ + return nlmsg_hdr(Message); +} diff --git a/src/linux/libnl-helpers/NetlinkSocket.cxx b/src/linux/libnl-helpers/NetlinkSocket.cxx new file mode 100644 index 00000000..73392d47 --- /dev/null +++ b/src/linux/libnl-helpers/NetlinkSocket.cxx @@ -0,0 +1,62 @@ + +#include + +using namespace Microsoft::Net::Netlink; + +/* static */ +NetlinkSocket +NetlinkSocket::Allocate() +{ + auto socket = nl_socket_alloc(); + return NetlinkSocket{ socket }; +} + +NetlinkSocket::NetlinkSocket(struct nl_sock* socket) : + Socket(socket) +{ +} + +NetlinkSocket::NetlinkSocket(NetlinkSocket&& other) : + Socket(other.Socket) +{ + other.Socket = nullptr; +} + +NetlinkSocket& +NetlinkSocket::operator=(NetlinkSocket&& other) +{ + if (this != &other) { + Reset(); + Socket = other.Socket; + other.Socket = nullptr; + } + + return *this; +} + +NetlinkSocket::~NetlinkSocket() +{ + Reset(); +} + +void +NetlinkSocket::Reset() +{ + if (Socket != nullptr) { + nl_socket_free(Socket); + Socket = nullptr; + } +} + +struct nl_sock* +NetlinkSocket::Release() noexcept +{ + auto socket = Socket; + Socket = nullptr; + return socket; +} + +NetlinkSocket::operator struct nl_sock *() const noexcept +{ + return Socket; +} diff --git a/src/linux/libnl-helpers/include/microsoft/net/netlink/NetlinkMessage.hxx b/src/linux/libnl-helpers/include/microsoft/net/netlink/NetlinkMessage.hxx new file mode 100644 index 00000000..1e2cb503 --- /dev/null +++ b/src/linux/libnl-helpers/include/microsoft/net/netlink/NetlinkMessage.hxx @@ -0,0 +1,104 @@ + +#ifndef MICROSOFT_NET_NETLINK_NETLINK_MESSAGE_HXX +#define MICROSOFT_NET_NETLINK_NETLINK_MESSAGE_HXX + +#include +#include + +namespace Microsoft::Net::Netlink +{ +/** + * @brief Heler for managing a netlink message, struct nl_msg. This class is not + * thread-safe. + */ +struct NetlinkMessage +{ + /** + * @brief The netlink message owned by this object. + */ + struct nl_msg* Message{ nullptr }; + + /** + * @brief Construct a new NetlinkMessage object that does not own a netlink + * message object. + */ + NetlinkMessage() = default; + + /** + * @brief Construct a new NetlinkMessage object that manages a pre-existing + * struct nl_msg object. Note that once construction is complete, this + * object owns the message and will free it when it is destroyed. + * + * @param message The netlink message to manage. + */ + NetlinkMessage(struct nl_msg* message); + + /** + * @brief Delete the copy constructor to enforce unique ownership. + */ + NetlinkMessage(const NetlinkMessage&) = delete; + + /** + * @brief Delete the copy assignment operator to enforce unique ownership. + * + * @return NetlinkMessage& + */ + NetlinkMessage& + operator=(const NetlinkMessage&) = delete; + + /** + * @brief Move-construct a new NetlinkMessage object. This takes ownership of + * the other instance. + * + * @param other The other instance to move from. + */ + NetlinkMessage(NetlinkMessage&& other); + + /** + * @brief Move-assign this instance from another instance. This takes + * ownership of the other instance. + * + * @param other The other instance to move from. + * @return NetlinkMessage& + */ + NetlinkMessage& + operator=(NetlinkMessage&& other); + + /** + * @brief Destroy the NetlinkMessage object, freeing the managed netlink + * message if it exists. + */ + ~NetlinkMessage(); + + /** + * @brief Reset the managed netlink message, freeing it if it exists. + */ + void + Reset(); + + /** + * @brief Release ownership of the managed netlink message, returning it to + * the caller. + */ + struct nl_msg* + Release() noexcept; + + /** + * @brief Implicit conversion operator to struct nl_msg *, allowing this + * class to be used in netlink API calls. + * + * @return struct nl_msg * The netlink message managed by this object. + */ + operator struct nl_msg *() const noexcept; + + /** + * @brief Obtain the message header. + * + * @return struct nlmsghdr* + */ + struct nlmsghdr* + Header() const noexcept; +}; +} // namespace Microsoft::Net::Netlink + +#endif // MICROSOFT_NET_NETLINK_NETLINK_MESSAGE_HXX diff --git a/src/linux/libnl-helpers/include/microsoft/net/netlink/NetlinkSocket.hxx b/src/linux/libnl-helpers/include/microsoft/net/netlink/NetlinkSocket.hxx new file mode 100644 index 00000000..dbea9662 --- /dev/null +++ b/src/linux/libnl-helpers/include/microsoft/net/netlink/NetlinkSocket.hxx @@ -0,0 +1,104 @@ + +#ifndef MICROSOFT_NET_NETLINK_NETLINK_SOCKET_HXX +#define MICROSOFT_NET_NETLINK_NETLINK_SOCKET_HXX + +#include +#include + +namespace Microsoft::Net::Netlink +{ +/** + * @brief Helper for managing a netlink socket, struct nl_sock. This class is + * not thread-safe. + */ +struct NetlinkSocket +{ + /** + * @brief The netlink socket owned by this object. + */ + struct nl_sock* Socket{ nullptr }; + + /** + * @brief Allocate a new struct nl_sock, and wrap it in a NetlinkSocket. + * + * @return NetlinkSocket + */ + static NetlinkSocket + Allocate(); + + /** + * @brief Construct a default NetlinkSocket object that does not own a + * netlink socket object. + */ + NetlinkSocket() = default; + + /** + * @brief Delete the copy constructor to enforce unique ownership. + */ + NetlinkSocket(const NetlinkSocket&) = delete; + + /** + * @brief Delete the copy assignment operator to enforce unique ownership. + * + * @return NetlinkSocket& + */ + NetlinkSocket& + operator=(const NetlinkSocket&) = delete; + + /** + * @brief Move-construct a new NetlinkSocket object. This takes ownership of + * the other instance. + * + * @param other The other instance to move from. + */ + NetlinkSocket(NetlinkSocket&& other); + + /** + * @brief Move-assign this instance from another instance. This takes + * ownership of the other instance. + * + * @param other The other instance to move from. + * @return NetlinkSocket& + */ + NetlinkSocket& + operator=(NetlinkSocket&& other); + + /** + * @brief Construct a new NetlinkSocket that manages a pre-existing struct + * nl_sock object. Note that once construction is complete, this object owns + * the socket and will free it when it is destroyed. + * + * @param socket The netlink socket to manage. + */ + NetlinkSocket(struct nl_sock* socket); + + /** + * @brief Destroy the NetlinkSocket object, freeing the managed netlink + * socket if it exists. + */ + virtual ~NetlinkSocket(); + + /** + * @brief Reset the managed netlink socket, freeing it if it exists. + */ + void + Reset(); + + /** + * @brief Release ownership of the managed netlink socket, returning it to + * the caller. + */ + struct nl_sock* + Release() noexcept; + + /** + * @brief Implicit conversion operator to struct nl_sock *, allowing this + * class to be used in netlink API calls. + * + * @return struct nl_sock * The netlink socket managed by this object. + */ + operator struct nl_sock *() const noexcept; +}; +} // namespace Microsoft::Net::Netlink + +#endif // MICROSOFT_NET_NETLINK_NETLINK_SOCKET_HXX diff --git a/src/linux/tools/CMakeLists.txt b/src/linux/tools/CMakeLists.txt new file mode 100644 index 00000000..80b615ac --- /dev/null +++ b/src/linux/tools/CMakeLists.txt @@ -0,0 +1,2 @@ + +add_subdirectory(apmonitor) diff --git a/src/linux/tools/apmonitor/CMakeLists.txt b/src/linux/tools/apmonitor/CMakeLists.txt new file mode 100644 index 00000000..5bdf66bb --- /dev/null +++ b/src/linux/tools/apmonitor/CMakeLists.txt @@ -0,0 +1,18 @@ + +add_executable(apmonitor-cli-linux) + +target_sources(apmonitor-cli-linux + PRIVATE + Main.cxx +) + +target_link_libraries(apmonitor-cli-linux + PRIVATE + plog::plog + wifi-apmanager-linux +) + +set_target_properties(apmonitor-cli-linux + PROPERTIES + OUTPUT_NAME apmonitor +) diff --git a/src/linux/tools/apmonitor/Main.cxx b/src/linux/tools/apmonitor/Main.cxx new file mode 100644 index 00000000..eedee271 --- /dev/null +++ b/src/linux/tools/apmonitor/Main.cxx @@ -0,0 +1,55 @@ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Microsoft::Net::Wifi::AccessPointDiscoveryAgent; +using Microsoft::Net::Wifi::IAccessPointDiscoveryAgentOperations; +using Microsoft::Net::Wifi::AccessPointDiscoveryAgentOperationsNetlink; + +int +main([[maybe_unused]] int argc, [[maybe_unused]] char *argv[]) +{ + // Configure console logging. + static plog::ColorConsoleAppender colorConsoleAppender{}; + plog::init(plog::verbose, &colorConsoleAppender); + + // Configure monitoring with the netlink protocol. + auto accessPointDiscoveryAgentOperationsNetlink{ std::make_unique() }; + auto accessPointDiscoveryAgent{ AccessPointDiscoveryAgent::Create(std::move(accessPointDiscoveryAgentOperationsNetlink)) }; + accessPointDiscoveryAgent->RegisterDiscoveryEventCallback([](auto&& presence, auto&& accessPointChanged) { + PLOG_INFO << std::format("{} {}", magic_enum::enum_name(presence), accessPointChanged != nullptr ? accessPointChanged->GetInterface() : ""); + }); + + LOG_INFO << "starting access point discovery agent"; + accessPointDiscoveryAgent->Start(); + + // Mask SIGTERM and SIGINT so they can be explicitly waited on from the main thread. + int signal; + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGTERM); + sigaddset(&mask, SIGINT); + + if (sigprocmask(SIG_BLOCK, &mask, nullptr) < 0) { + LOG_ERROR << "failed to block terminate signals"; + return -1; + } + + // Wait for the process to be signaled to exit. + while (sigwait(&mask, &signal) != 0) + ; + + // Received interrupt or terminate signal, so shut down. + accessPointDiscoveryAgent->Stop(); + + return 0; +} diff --git a/src/linux/wifi/CMakeLists.txt b/src/linux/wifi/CMakeLists.txt index 6b0579e7..1e8a6c5d 100644 --- a/src/linux/wifi/CMakeLists.txt +++ b/src/linux/wifi/CMakeLists.txt @@ -1,2 +1,3 @@ +add_subdirectory(apmanager) add_subdirectory(core) diff --git a/src/linux/wifi/apmanager/AccessPointDiscoveryAgentOperationsNetlink.cxx b/src/linux/wifi/apmanager/AccessPointDiscoveryAgentOperationsNetlink.cxx new file mode 100644 index 00000000..00e28bda --- /dev/null +++ b/src/linux/wifi/apmanager/AccessPointDiscoveryAgentOperationsNetlink.cxx @@ -0,0 +1,346 @@ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Microsoft::Net::Wifi; + +using Microsoft::Net::Netlink::NetlinkMessage; +using Microsoft::Net::Netlink::NetlinkSocket; + +AccessPointDiscoveryAgentOperationsNetlink::AccessPointDiscoveryAgentOperationsNetlink() : + m_cookie(CookieValid) +{} + +AccessPointDiscoveryAgentOperationsNetlink::~AccessPointDiscoveryAgentOperationsNetlink() +{ + RequestNetlinkProcessingLoopStop(); + + // Explicitly stop the netlink message processing thread to force the thread + // to tear down predictably. This isn't technically required since the + // member variables are ordered such that the thread will be stopped before + // the netlink socket is destroyed, but it is good practice to be explicit + // and avoids any potential issues if members are re-ordered. + Stop(); + + // Invalidate the cookie as a rudimentary way to detect use-after-free + // within the netlink thread. + m_cookie = CookieInvalid; +} + +void +AccessPointDiscoveryAgentOperationsNetlink::RequestNetlinkProcessingLoopStop() +{ + if (m_eventLoopStopFd == -1) { + return; + } + + constexpr uint64_t StopValue{ 1 }; + + // Write any value to the eventfd to signal the netlink processing loop to + // stop. The value itself is meaningless, but the write to the file + // descriptor will interrupt the epoll_wait call and set the stop flag. + ssize_t numWritten = write(m_eventLoopStopFd, &StopValue, sizeof StopValue); + if (numWritten != sizeof StopValue) { + const auto err = errno; + LOG_ERROR << std::format("Failed to write to event loop stop fd with error {} ({})", err, strerror(err)); + } +} + +void +AccessPointDiscoveryAgentOperationsNetlink::Start(AccessPointPresenceEventCallback accessPointPresenceEventCallback) +{ + if (m_netlinkMessageProcessingThread.joinable()) { + LOG_WARNING << "Netlink message processing thread is already running"; + Stop(); + } + + // Open a new netlink socket with the routing/networking family group. + auto netlinkSocket{ NetlinkSocket::Allocate() }; + if (netlinkSocket == nullptr) { + // TODO: this function needs to signal the error either through its return type, or an exception. + const auto err = errno; + LOG_ERROR << std::format("Failed to allocate new netlink socket with error {} ({})", err, strerror(err)); + return; + } + + // Connect the socket to the netlink routing family. + int ret = nl_connect(netlinkSocket, NETLINK_ROUTE); + if (ret < 0) { + const auto err = errno; + LOG_ERROR << std::format("Failed to connect netlink socket with error {} ({})", err, strerror(err)); + return; + } + + // Subscribe to the link group of messages. + ret = nl_socket_add_membership(netlinkSocket, RTMGRP_LINK); + if (ret < 0) { + const auto err = errno; + LOG_ERROR << std::format("Failed to add netlink socket membership with error {} ({})", err, strerror(err)); + return; + } + + // Update the access point presence callback for the netlink message handler to use. + // Note: This is not thread-safe. + m_accessPointPresenceCallback = std::move(accessPointPresenceEventCallback); + m_netlinkMessageProcessingThread = std::jthread([this, netlinkSocket = std::move(netlinkSocket)](std::stop_token stopToken) mutable { + ProcessNetlinkMessagesThread(std::move(netlinkSocket), std::move(stopToken)); + }); +} + +void +AccessPointDiscoveryAgentOperationsNetlink::Stop() +{ + if (m_netlinkMessageProcessingThread.joinable()) { + LOG_VERBOSE << "Stopping netlink message processing thread"; + RequestNetlinkProcessingLoopStop(); + m_netlinkMessageProcessingThread.request_stop(); + m_netlinkMessageProcessingThread.join(); + } +} + +std::future>> +AccessPointDiscoveryAgentOperationsNetlink::ProbeAsync() +{ + std::promise>> probePromise{}; + auto probeFuture = probePromise.get_future(); + + // TODO: implement this. + std::vector> accessPoints{}; + probePromise.set_value(std::move(accessPoints)); + + return probeFuture; +} + +int +AccessPointDiscoveryAgentOperationsNetlink::ProcessNetlinkMessage(struct nl_msg *netlinkMessage, AccessPointPresenceEventCallback &accessPointPresenceEventCallback) +{ + std::shared_ptr accessPoint{ nullptr }; + AccessPointPresenceEvent accessPointPresenceEvent; + auto netlinkMessageHeader{ nlmsg_hdr(netlinkMessage) }; + + if (netlinkMessageHeader == nullptr) { + LOG_ERROR << "Netlink message header is null, ignoring message"; + return NL_SKIP; + } + + auto interfaceNameAttribute = nlmsg_find_attr(netlinkMessageHeader, sizeof *netlinkMessageHeader, IFLA_IFNAME); + if (interfaceNameAttribute == nullptr) { + LOG_ERROR << "Netlink message does not contain interface name attribute, ignoring message"; + return NL_SKIP; + } + + const auto *interfaceName = static_cast(RTA_DATA(interfaceNameAttribute)); + const auto *interfaceInfoMessage{ static_cast(NLMSG_DATA(netlinkMessageHeader)) }; + LOG_VERBOSE << std::format("Received netlink message with type {}, interface {}, index {}", netlinkMessageHeader->nlmsg_type, interfaceName, interfaceInfoMessage->ifi_index); + + switch (netlinkMessageHeader->nlmsg_type) { + case RTM_NEWLINK: + accessPointPresenceEvent = AccessPointPresenceEvent::Arrived; + // TODO: process message + break; + case RTM_DELLINK: + accessPointPresenceEvent = AccessPointPresenceEvent::Departed; + // TODO: process message + break; + default: + PLOG_VERBOSE << std::format("Ignoring netlink message with type {}", netlinkMessageHeader->nlmsg_type); + return NL_SKIP; + } + + if (accessPointPresenceEventCallback != nullptr) { + LOG_VERBOSE << std::format("Invoking access point presence event callback with event args 'presence={}, accessPointChanged={}'", magic_enum::enum_name(accessPointPresenceEvent), accessPoint != nullptr ? accessPoint->GetInterface() : ""); + accessPointPresenceEventCallback(accessPointPresenceEvent, std::move(accessPoint)); + } + + return NL_OK; +} + +/* static */ +int +AccessPointDiscoveryAgentOperationsNetlink::ProcessNetlinkMessagesCallback(struct nl_msg *netlinkMessage, void *contextArgument) +{ + LOG_VERBOSE << "Received netlink message callback"; + + // Validate the context argument and cookie to ensure the discovery agent is still alive. + auto *instance{ static_cast(contextArgument) }; + if (instance == nullptr) { + static constexpr auto errorMessage{ "Netlink message callback context is null; this is a bug!" }; + LOG_FATAL << errorMessage; + throw std::runtime_error(errorMessage); + } else if (instance->m_cookie != CookieValid) { + LOG_ERROR << "Netlink message callback context cookie is invalid; discovery agent instance has been destroyed"; + return NL_STOP; + } + + auto ret = instance->ProcessNetlinkMessage(netlinkMessage, instance->m_accessPointPresenceCallback); + LOG_VERBOSE << std::format("Processed netlink message with result {}", ret); + + return ret; +} + +void +AccessPointDiscoveryAgentOperationsNetlink::ProcessNetlinkMessagesThread(NetlinkSocket netlinkSocket, std::stop_token stopToken) +{ + // Disable sequence number checking since it is not required for event notifications. + nl_socket_disable_seq_check(netlinkSocket); + + // Register a callback to be invoked for all valid, parsed netlink messages. + int ret = nl_socket_modify_cb(netlinkSocket, NL_CB_VALID, NL_CB_CUSTOM, ProcessNetlinkMessagesCallback, this); + if (ret < 0) { + const auto err = errno; + LOG_ERROR << std::format("Failed to modify netlink socket callback with error {} ({})", err, strerror(err)); + return; + } + + // Put the socket into non-blocking mode since callbacks have been configured. + ret = nl_socket_set_nonblocking(netlinkSocket); + if (ret < 0) { + const auto err = errno; + LOG_ERROR << std::format("Failed to set netlink socket to non-blocking mode with error {} ({})", err, strerror(err)); + return; + } + + // Obtain the underlying file descriptor for the netlink socket. + auto netlinkSocketFileDescriptor = nl_socket_get_fd(netlinkSocket); + if (netlinkSocketFileDescriptor < 0) { + const auto err = errno; + LOG_ERROR << std::format("Failed to obtain netlink socket file descriptor with error {} ({})", err, strerror(err)); + return; + } + + // Configure epoll to monitor the netlink socket for readability. + int fdEpoll = epoll_create1(0); + if (fdEpoll < 0) { + const auto err = errno; + LOG_ERROR << std::format("Failed to create epoll instance with error {} ({})", err, strerror(err)); + return; + } + + // Close the epoll file descriptor when this function returns. + auto closeEpollOnExit = notstd::ScopeExit([fdEpoll] { + close(fdEpoll); + }); + + // Allocate space for two events: one for the netlink socket and one for the eventfd to stop the event loop. + static constexpr int EpollEventsMax{ 2 }; + struct epoll_event events[EpollEventsMax] = {}; + + // Populate the epoll event for the netlink socket. + struct epoll_event *epollEventNetlinkSocket = &events[0]; + epollEventNetlinkSocket->events = EPOLLIN; + epollEventNetlinkSocket->data.fd = netlinkSocketFileDescriptor; + + // Register the netlink socket event with epoll. + ret = epoll_ctl(fdEpoll, EPOLL_CTL_ADD, netlinkSocketFileDescriptor, epollEventNetlinkSocket); + if (ret < 0) { + const auto err = errno; + LOG_ERROR << std::format("Failed to register netlink socket event with epoll with error {} ({})", err, strerror(err)); + return; + } + + // Create a new epoll event to allow stopping the processing loop using eventfd. + int eventLoopStopFd = eventfd(0, 0); + if (eventLoopStopFd < 0) { + const auto err = errno; + LOG_ERROR << std::format("Failed to create eventfd for stopping event loop with error {} ({})", err, strerror(err)); + return; + } + + // Close the eventfd when this function returns and clear out the member. + auto closeNetlinkMessageProcessingLoopStopFdOnExit = notstd::ScopeExit([this, eventLoopStopFd] { + close(eventLoopStopFd); + m_eventLoopStopFd = -1; + }); + + // Populate the epoll event for the event loop stop file descriptor. + struct epoll_event *epollEventLoopStopFd = &events[1]; + epollEventLoopStopFd->events = EPOLLIN; + epollEventLoopStopFd->data.fd = eventLoopStopFd; + + // Register the event loop stop event with epoll. + ret = epoll_ctl(fdEpoll, EPOLL_CTL_ADD, eventLoopStopFd, epollEventLoopStopFd); + if (ret < 0) { + const auto err = errno; + LOG_ERROR << std::format("Failed to register event loop stop fd event with epoll with error {} ({})", err, strerror(err)); + return; + } + + // Save the event loop stop fd so it can be signaled externally to stop the event loop. + m_eventLoopStopFd = eventLoopStopFd; + + bool stopRequested = false; + + // Event loop to wait for netlink messages and process them. + for (;;) { + if (stopToken.stop_requested() || stopRequested) { + LOG_VERBOSE << "Netlink message processing thread has been requested to stop"; + break; + } + + // Wait for at least one event file descriptor to become ready. + int numEventsReady = epoll_wait(fdEpoll, events, EpollEventsMax, -1); + if (numEventsReady < 0) { + const auto err = errno; + if (err == EINTR) { + LOG_VERBOSE << "Interrupted while waiting for epoll events, retrying"; + continue; + } + + LOG_ERROR << std::format("Failed to wait for epoll events with error {} ({})", err, strerror(err)); + break; + } + + // Determine which file descriptor(s) became ready and handle them. + for (auto i = 0; i < numEventsReady; i++) { + const auto &eventReady = events[i]; + if (eventReady.data.fd == netlinkSocketFileDescriptor) { + HandleNetlinkSocketReady(netlinkSocket); + } else if (eventReady.data.fd == eventLoopStopFd) { + stopRequested = true; + } else { + LOG_WARNING << std::format("Unknown file descriptor {} is ready for reading", eventReady.data.fd); + } + } + } + + LOG_INFO << "Netlink message processing thread has exited"; +} + +void +AccessPointDiscoveryAgentOperationsNetlink::HandleNetlinkSocketReady(NetlinkSocket &netlinkSocket) +{ + LOG_VERBOSE << "Handling netlink socket read availabilty"; + + // Read all pending netlink messages. + for (;;) { + int ret = nl_recvmsgs_default(netlinkSocket); + if (ret < 0) { + const auto err = errno; + if (err == EINTR) { + LOG_VERBOSE << "Interrupted while waiting for netlink messages, retrying"; + continue; + } else { + LOG_ERROR << std::format("Failed to receive netlink messages with error {} ({})", err, strerror(err)); + break; + } + } else { + LOG_VERBOSE << "Successfully processed netlink messages"; + break; + } + } +} diff --git a/src/linux/wifi/apmanager/CMakeLists.txt b/src/linux/wifi/apmanager/CMakeLists.txt new file mode 100644 index 00000000..5843cfb1 --- /dev/null +++ b/src/linux/wifi/apmanager/CMakeLists.txt @@ -0,0 +1,34 @@ + +add_library(wifi-apmanager-linux STATIC "") + +set(WIFI_APMANAGER_LINUX_PUBLIC_INCLUDE ${CMAKE_CURRENT_LIST_DIR}/include) +set(WIFI_APMANAGER_LINUX_PUBLIC_INCLUDE_SUFFIX microsoft/net/wifi) +set(WIFI_APMANAGER_LINUX_PUBLIC_INCLUDE_PREFIX ${WIFI_APMANAGER_LINUX_PUBLIC_INCLUDE}/${WIFI_APMANAGER_LINUX_PUBLIC_INCLUDE_SUFFIX}) + +target_sources(wifi-apmanager-linux + PRIVATE + AccessPointDiscoveryAgentOperationsNetlink.cxx + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${WIFI_APMANAGER_LINUX_PUBLIC_INCLUDE} + FILES + ${WIFI_APMANAGER_LINUX_PUBLIC_INCLUDE_PREFIX}/AccessPointDiscoveryAgentOperationsNetlink.hxx +) + +target_link_libraries(wifi-apmanager-linux + PRIVATE + nl + notstd + plog::plog + PUBLIC + libnl-helpers + wifi-apmanager + wifi-core +) + +install( + TARGETS wifi-apmanager-linux + EXPORT ${PROJECT_NAME} + FILE_SET HEADERS + PUBLIC_HEADER DESTINATION "${NETREMOTE_DIR_INSTALL_PUBLIC_HEADER_BASE}/${WIFI_APMANAGER_LINUX_PUBLIC_INCLUDE_SUFFIX}" +) diff --git a/src/linux/wifi/apmanager/include/microsoft/net/wifi/AccessPointDiscoveryAgentOperationsNetlink.hxx b/src/linux/wifi/apmanager/include/microsoft/net/wifi/AccessPointDiscoveryAgentOperationsNetlink.hxx new file mode 100644 index 00000000..e1d78e87 --- /dev/null +++ b/src/linux/wifi/apmanager/include/microsoft/net/wifi/AccessPointDiscoveryAgentOperationsNetlink.hxx @@ -0,0 +1,97 @@ + +#ifndef ACCESS_POINT_DISCOVERY_AGENT_OPERATIONS_NETLINK_HXX +#define ACCESS_POINT_DISCOVERY_AGENT_OPERATIONS_NETLINK_HXX + +#include +#include +#include + +#include +#include +#include +#include + +namespace Microsoft::Net::Wifi +{ +/** + * @brief Access point discovery agent operations that use netlink for + * discovering devices and monitoring device presence. + * + * Note that this class is not thread-safe. + */ +struct AccessPointDiscoveryAgentOperationsNetlink : + public IAccessPointDiscoveryAgentOperations +{ + AccessPointDiscoveryAgentOperationsNetlink(); + + virtual ~AccessPointDiscoveryAgentOperationsNetlink(); + + void + Start(AccessPointPresenceEventCallback accessPointPresenceEventCallback) override; + + void + Stop() override; + + std::future>> + ProbeAsync() override; + +private: + /** + * @brief Request that the netlink processing loop stop. + */ + void + RequestNetlinkProcessingLoopStop(); + + /** + * @brief Handles when the netlink socket is ready for reading. + * + * @param netlinkSocket The netlink socket that is ready for reading. + */ + void + HandleNetlinkSocketReady(Microsoft::Net::Netlink::NetlinkSocket &netlinkSocket); + + /** + * @brief Thread function for processing netlink messages. + * + * @param netlinkSocket The netlink socket to use for processing messages. + * @param stopToken The stop token to use for stopping the thread. + */ + void + ProcessNetlinkMessagesThread(Microsoft::Net::Netlink::NetlinkSocket netlinkSocket, std::stop_token stopToken); + + /** + * @brief C-style function for use with netlink message callbacks. + * + * @param netlinkMessage The netlink message being processed. + * @param contextArgument The callback context argument. This must be a + * pointer to the class instance (AccessPointDiscoveryAgentOperationsNetlink*) + * which owns the callback. + * @return int + */ + static int + ProcessNetlinkMessagesCallback(struct nl_msg *netlinkMessage, void *contextArgument); + + /** + * @brief Process a single netlink message. + * + * @param netlinkMessage The netlink message to process. + * @param accessPointPresenceEventCallback The callback to invoke when an access point presence event occurs. + */ + int + ProcessNetlinkMessage(struct nl_msg *netlinkMessage, AccessPointPresenceEventCallback &accessPointPresenceEventCallback); + +private: + // Cookie used to validate that the callback context is valid. + static constexpr uint32_t CookieValid{ 0x8BADF00Du }; + // Cookie used to invalidate the callback context. + static constexpr uint32_t CookieInvalid{ 0xDEADBEEFu }; + + uint32_t m_cookie{ CookieInvalid }; + AccessPointPresenceEventCallback m_accessPointPresenceCallback{ nullptr }; + + int m_eventLoopStopFd{ -1 }; + std::jthread m_netlinkMessageProcessingThread; +}; +} // namespace Microsoft::Net::Wifi + +#endif // ACCESS_POINT_DISCOVERY_AGENT_OPERATIONS_NETLINK_HXX diff --git a/tests/unit/linux/wifi/CMakeLists.txt b/tests/unit/linux/wifi/CMakeLists.txt index 6b0579e7..1e8a6c5d 100644 --- a/tests/unit/linux/wifi/CMakeLists.txt +++ b/tests/unit/linux/wifi/CMakeLists.txt @@ -1,2 +1,3 @@ +add_subdirectory(apmanager) add_subdirectory(core) diff --git a/tests/unit/linux/wifi/apmanager/CMakeLists.txt b/tests/unit/linux/wifi/apmanager/CMakeLists.txt new file mode 100644 index 00000000..bfe33887 --- /dev/null +++ b/tests/unit/linux/wifi/apmanager/CMakeLists.txt @@ -0,0 +1,24 @@ + +add_executable(wifi-apmanager-linux-test-unit) + +target_sources(wifi-apmanager-linux-test-unit + PRIVATE + Main.cxx + TestAccessPointDiscoveryAgentOperationsNetlink.cxx +) + +target_include_directories(wifi-apmanager-linux-test-unit + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(wifi-apmanager-linux-test-unit + PRIVATE + Catch2::Catch2 + magic_enum::magic_enum + plog::plog + strings + wifi-apmanager-linux +) + +catch_discover_tests(wifi-apmanager-linux-test-unit) diff --git a/tests/unit/linux/wifi/apmanager/Main.cxx b/tests/unit/linux/wifi/apmanager/Main.cxx new file mode 100644 index 00000000..4ecc7af1 --- /dev/null +++ b/tests/unit/linux/wifi/apmanager/Main.cxx @@ -0,0 +1,15 @@ + +#include +#include +#include +#include +#include + +int main(int argc, char* argv[]) +{ + static plog::ColorConsoleAppender colorConsoleAppender{}; + + plog::init(plog::verbose, &colorConsoleAppender); + + return Catch::Session().run(argc, argv); +} diff --git a/tests/unit/linux/wifi/apmanager/TestAccessPointDiscoveryAgentOperationsNetlink.cxx b/tests/unit/linux/wifi/apmanager/TestAccessPointDiscoveryAgentOperationsNetlink.cxx new file mode 100644 index 00000000..871c0040 --- /dev/null +++ b/tests/unit/linux/wifi/apmanager/TestAccessPointDiscoveryAgentOperationsNetlink.cxx @@ -0,0 +1,145 @@ + +#include +#include + +#include + +#include + +TEST_CASE("Create AccessPointDiscoveryAgentOperationsNetlink", "[wifi][core][apmanager]") +{ + SECTION("Create doesn't cause a crash") + { + using namespace Microsoft::Net::Wifi; + + REQUIRE_NOTHROW(AccessPointDiscoveryAgentOperationsNetlink{}); + } +} + +TEST_CASE("Destroy AccessPointDiscoveryAgentOperationsNetlink", "[wifi][core][apmanager]") +{ + using namespace Microsoft::Net::Wifi; + + SECTION("Destroy doesn't cause a crash") + { + std::optional accessPointDiscoveryAgent(std::in_place); + REQUIRE_NOTHROW(accessPointDiscoveryAgent.reset()); + } +} + +TEST_CASE("AccessPointDiscoveryAgentOperationsNetlink::Start", "[wifi][core][apmanager]") +{ + using namespace Microsoft::Net::Wifi; + + SECTION("Start doesn't cause a crash") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent; + REQUIRE_NOTHROW(accessPointDiscoveryAgent.Start([](auto &&, auto &&) {})); + } + + SECTION("Start doesn't cause a crash when called twice") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent; + accessPointDiscoveryAgent.Start([](auto &&, auto &&) {}); + REQUIRE_NOTHROW(accessPointDiscoveryAgent.Start([](auto &&, auto &&) {})); + } + + SECTION("Start doesn't cause a crash when called with a null callback") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent; + REQUIRE_NOTHROW(accessPointDiscoveryAgent.Start(nullptr)); + } +} + +TEST_CASE("AccessPointDiscoveryAgentOperationsNetlink::Stop", "[wifi][core][apmanager]") +{ + using namespace Microsoft::Net::Wifi; + + SECTION("Stop doesn't cause a crash when Start hasn't been called") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + REQUIRE_NOTHROW(accessPointDiscoveryAgent.Stop()); + } + + SECTION("Stop doesn't cause a crash when Start has been called") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + accessPointDiscoveryAgent.Start([](auto &&, auto &&) {}); + REQUIRE_NOTHROW(accessPointDiscoveryAgent.Stop()); + } + + SECTION("Stop doesn't cause a crash when called twice") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + accessPointDiscoveryAgent.Start([](auto &&, auto &&) {}); + accessPointDiscoveryAgent.Stop(); + REQUIRE_NOTHROW(accessPointDiscoveryAgent.Stop()); + } + + SECTION("Stop doesn't cause a crash when called with a null callback") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + accessPointDiscoveryAgent.Start(nullptr); + REQUIRE_NOTHROW(accessPointDiscoveryAgent.Stop()); + } +} + +TEST_CASE("AccessPointDiscoveryAgentOperationsNetlink::ProbeAsync", "[wifi][core][apmanager]") +{ + using namespace Microsoft::Net::Wifi; + + SECTION("ProbeAsync doesn't cause a crash when Start hasn't been called") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + REQUIRE_NOTHROW(accessPointDiscoveryAgent.ProbeAsync()); + } + + SECTION("ProbeAsync doesn't cause a crash when Start has been called") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + accessPointDiscoveryAgent.Start([](auto &&, auto &&) {}); + REQUIRE_NOTHROW(accessPointDiscoveryAgent.ProbeAsync()); + } + + SECTION("ProbeAsync doesn't cause a crash when Stop has been called") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + accessPointDiscoveryAgent.Stop(); + REQUIRE_NOTHROW(accessPointDiscoveryAgent.ProbeAsync()); + } + + SECTION("ProbeAsync doesn't cause a crash when called twice") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + accessPointDiscoveryAgent.Start([](auto &&, auto &&) {}); + accessPointDiscoveryAgent.ProbeAsync(); + REQUIRE_NOTHROW(accessPointDiscoveryAgent.ProbeAsync()); + } + + SECTION("ProbeAsync doesn't cause a crash when called after a Start/Stop sequence") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + accessPointDiscoveryAgent.Start(nullptr); + accessPointDiscoveryAgent.Stop(); + REQUIRE_NOTHROW(accessPointDiscoveryAgent.ProbeAsync()); + } + + SECTION("ProbeAsync returns a valid future") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + accessPointDiscoveryAgent.Start(nullptr); + REQUIRE(accessPointDiscoveryAgent.ProbeAsync().valid()); + } + + SECTION("ProbeAsync result can be obtained without causing a crash") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + REQUIRE_NOTHROW(accessPointDiscoveryAgent.ProbeAsync().get()); + } + + SECTION("ProbeAsync result is valid") + { + AccessPointDiscoveryAgentOperationsNetlink accessPointDiscoveryAgent{}; + REQUIRE_NOTHROW(accessPointDiscoveryAgent.ProbeAsync().get().clear()); + } +}