Skip to content

Commit

Permalink
Merge pull request #302 from microsoft/wpaevents
Browse files Browse the repository at this point in the history
Add ability to listen to WPA (hostapd, wpa_supplicant) events
  • Loading branch information
abeltrano authored Jul 2, 2024
2 parents 7bf1cf3 + becbfa0 commit 262aec2
Show file tree
Hide file tree
Showing 14 changed files with 639 additions and 9 deletions.
6 changes: 6 additions & 0 deletions src/linux/wpa-controller/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ target_sources(wpa-controller
WpaController.cxx
WpaControlSocket.cxx
WpaControlSocketConnection.cxx
WpaEventHandler.cxx
WpaEventListenerProxy.cxx
WpaKeyValuePair.cxx
WpaParsingUtilities.cxx
WpaParsingUtilities.hxx
Expand All @@ -29,6 +31,7 @@ target_sources(wpa-controller
FILES
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/Hostapd.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/IHostapd.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/IWpaEventListener.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/ProtocolHostapd.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/ProtocolWpa.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/ProtocolWpaConfig.hxx
Expand All @@ -40,6 +43,9 @@ target_sources(wpa-controller
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/WpaControlSocket.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/WpaControlSocketConnection.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/WpaCore.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/WpaEvent.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/WpaEventHandler.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/WpaEventListenerProxy.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/WpaKeyValuePair.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/WpaResponse.hxx
${WPA_CONTROLLER_PUBLIC_INCLUDE_PREFIX}/WpaResponseParser.hxx
Expand Down
28 changes: 27 additions & 1 deletion src/linux/wpa-controller/Hostapd.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <Wpa/WpaCommandSet.hxx>
#include <Wpa/WpaCommandStatus.hxx>
#include <Wpa/WpaControlSocket.hxx>
#include <Wpa/WpaControlSocketConnection.hxx>
#include <Wpa/WpaCore.hxx>
#include <Wpa/WpaResponseStatus.hxx>
#include <magic_enum.hpp>
Expand All @@ -34,8 +35,27 @@ Hostapd::IsManagingInterface(std::string_view interfaceName) noexcept

Hostapd::Hostapd(std::string_view interfaceName) :
m_interface(interfaceName),
m_controller(interfaceName, WpaType::Hostapd)
m_controller(interfaceName, WpaType::Hostapd),
m_eventListenerProxy(WpaEventListenerProxy::Create(*this))
{
auto controlSocketConnection{ WpaControlSocketConnection::TryCreate(interfaceName, m_controller.ControlSocketPath()) };
if (!controlSocketConnection) {
throw HostapdException("Failed to create hostapd event handler control socket connection");
}

auto eventHandler{ std::make_unique<WpaEventHandler>(std::move(controlSocketConnection), WpaType::Hostapd) };
if (!eventHandler) {
throw HostapdException("Failed to create hostapd event handler");
}

m_eventHandlerRegistrationToken = eventHandler->RegisterEventListener(m_eventListenerProxy->weak_from_this());
m_eventHandler = std::move(eventHandler);
m_eventHandler->StartListening();
}

Hostapd::~Hostapd()
{
m_eventHandler->UnregisterEventListener(m_eventHandlerRegistrationToken);
}

std::string_view
Expand Down Expand Up @@ -506,3 +526,9 @@ Hostapd::GetIpAddress() const noexcept
{
return m_ownIpAddress;
}

void
Hostapd::OnWpaEvent(WpaEventSender* sender, const WpaEventArgs* eventArgs)
{
LOGD << std::format("Hostapd event @ {}: {:#08x} {}", eventArgs->Timestamp, reinterpret_cast<uintptr_t>(sender), eventArgs->Event.Payload);
}
9 changes: 8 additions & 1 deletion src/linux/wpa-controller/WpaControlSocketConnection.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ WpaControlSocketConnection::~WpaControlSocketConnection()
}

WpaControlSocketConnection::WpaControlSocketConnection(std::string_view interfaceName, std::filesystem::path controlSocketPathDir) :
m_controlSocketPath(std::move(controlSocketPathDir) / interfaceName)
m_controlSocketPath(std::move(controlSocketPathDir) / interfaceName),
m_interfaceName(interfaceName)
{
}

Expand Down Expand Up @@ -63,3 +64,9 @@ WpaControlSocketConnection::Disconnect() noexcept
m_controlSocket = nullptr;
}
}

std::string_view
WpaControlSocketConnection::GetInterfaceName() const noexcept
{
return m_interfaceName;
}
285 changes: 285 additions & 0 deletions src/linux/wpa-controller/WpaEventHandler.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@

#include <array>
#include <chrono>
#include <cstdint>
#include <format>
#include <memory>
#include <mutex>

#include <Wpa/ProtocolWpa.hxx>
#include <Wpa/WpaEventHandler.hxx>
#include <notstd/Scope.hxx>
#include <plog/Log.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/types.h>
#include <unistd.h>
#include <wpa_ctrl.h>

using namespace Wpa;

WpaEventHandler::WpaEventHandler(std::unique_ptr<WpaControlSocketConnection> wpaControlSocketConnection, WpaType wpaType) :
m_wpaControlSocketConnection(std::move(wpaControlSocketConnection)),
m_wpaType(wpaType)
{
}

WpaEventHandler::~WpaEventHandler()
{
StopListening();
}

WpaEventListenerRegistrationToken
WpaEventHandler::RegisterEventListener(std::weak_ptr<IWpaEventListener> wpaEventListener)
{
std::unique_lock eventListenerWriteLock{ m_eventListenerStateGate };

auto wpaEventListenerRegistrationToken = m_eventListenerRegistrationTokenNext++;
m_eventListeners[wpaEventListenerRegistrationToken] = wpaEventListener;
LOGD << std::format("Registered WPA event listener with eventListenerRegistrationToken '{}' on interface '{}'", wpaEventListenerRegistrationToken, m_wpaControlSocketConnection->GetInterfaceName());

return wpaEventListenerRegistrationToken;
}

void
WpaEventHandler::UnregisterEventListener(WpaEventListenerRegistrationToken wpaEventListenerRegistrationToken)
{
std::unique_lock eventListenerWriteLock{ m_eventListenerStateGate };

const auto numRemoved = m_eventListeners.erase(wpaEventListenerRegistrationToken);
if (numRemoved == 0) {
LOGW << std::format("Attempted to unregister a WPA event listener that was not registered for interface '{}'.", m_wpaControlSocketConnection->GetInterfaceName());
} else {
LOGD << std::format("Unregistered WPA event listener with eventListenerRegistrationToken '{}' on interface '{}'", wpaEventListenerRegistrationToken, m_wpaControlSocketConnection->GetInterfaceName());
}
}

void
WpaEventHandler::StartListening()
{
std::unique_lock eventListenerWriteLock{ m_eventListenerStateGate };

m_eventListenerThread = std::jthread([this](std::stop_token stopToken) mutable {
ProcessEvents(*m_wpaControlSocketConnection, std::move(stopToken));
});
}

void
WpaEventHandler::StopListening()
{
std::shared_lock eventListenerReadLock{ m_eventListenerStateGate };

// Check if the eventfd file descriptor for signaling thread stop has been created. If so, signal the thread to stop.
if (m_fdEventFdStop != -1) {
uint64_t value{ EventFdStopRequestValue };
ssize_t numWritten = write(m_fdEventFdStop, &value, sizeof value);
if (numWritten != sizeof value) {
LOGE << std::format("Failed to write eventfd stop signal value for interface '{}'.", m_wpaControlSocketConnection->GetInterfaceName());
}
}

if (m_eventListenerThread.joinable()) {
m_eventListenerThread.request_stop();
m_eventListenerThread.join();
m_eventListenerThread = {};
}
}

void
WpaEventHandler::ProcessNextEvent(WpaControlSocketConnection& wpaControlSocketConnection)
{
const auto InterfaceName{ wpaControlSocketConnection.GetInterfaceName() };
LOGD << std::format("Processing pending WPA event on interface '{}'.", InterfaceName);

// Allocate a buffer for the event data, and subtract one to leave room for the null terminator.
std::array<char, ProtocolWpa::EventSizeMax + 1> wpaEventBuffer{};
std::size_t wpaEventBufferSize = std::size(wpaEventBuffer) - 1;

// Retrieve the next WPA event from the control socket.
int ret = wpa_ctrl_recv(*m_wpaControlSocketConnection, std::data(wpaEventBuffer), &wpaEventBufferSize);
if (ret < 0) {
LOGE << std::format("Failed to receive WPA event on interface '{}'.", InterfaceName);
return;
}

// Explicitly null-terminate the buffer in case bad data was provided.
wpaEventBuffer[wpaEventBufferSize] = '\0';

// Record the time this event was received.
const auto timestampNow{ std::chrono::system_clock::now() };
std::string wpaEventPayload{ std::data(wpaEventBuffer), wpaEventBufferSize };

// Create a WPA event args object to pass to the listeners.
WpaEventArgs wpaEventArgs{
.Timestamp = timestampNow,
.Event = {
.Source = m_wpaType,
.Payload = { std::data(wpaEventBuffer), wpaEventBufferSize },
},
};

// Make a copy of the event listeners to minimuze the time we hold the state lock. This is safe since the event
// listeners are weak_ptrs which we later attempt to promote to a full shared_ptr before de-referencing them.
decltype(m_eventListeners) eventListeners{};
std::vector<decltype(m_eventListeners)::key_type> eventListenerRegistrationTokensExpired{};
{
std::shared_lock eventListenerReadLock{ m_eventListenerStateGate };
eventListeners = m_eventListeners;
}

// Invoke each registered event listener with the event args.
for (const auto& [eventListenerRegistrationToken, eventListenerWeak] : eventListeners) {
std::shared_ptr<IWpaEventListener> eventListener = eventListenerWeak.lock();
if (!eventListener) {
LOGW << std::format("WPA event listener with registration token '{}' on interface '{}' expired; removing it", eventListenerRegistrationToken, InterfaceName);
eventListenerRegistrationTokensExpired.push_back(eventListenerRegistrationToken);
continue;
}

// Fire the event on the listener.
eventListener->OnWpaEvent(this, &wpaEventArgs);
}

// Remove any expired listeners.
if (!std::empty(eventListenerRegistrationTokensExpired)) {
std::unique_lock eventListenerWriteLock{ m_eventListenerStateGate };
for (const auto& eventListenerRegistrationToken : eventListenerRegistrationTokensExpired) {
m_eventListeners.erase(eventListenerRegistrationToken);
}
}
}

void
WpaEventHandler::ProcessEvents(WpaControlSocketConnection& wpaControlSocketConnection, std::stop_token stopToken)
{
const auto interfaceName{ wpaControlSocketConnection.GetInterfaceName() };
LOGD << std::format("WPA event listener thread for interface '{}' started", interfaceName);
auto logOnExit = notstd::ScopeExit([&] {
LOGD << std::format("WPA event listener thread for interface '{}' stopped", interfaceName);
});

// One event for eventfd stop signaling, and one for WPA control socket event signaling.
static constexpr int EpollEventsMax{ 2 };

const auto InterfaceName{ wpaControlSocketConnection.GetInterfaceName() };

std::array<epoll_event, EpollEventsMax> epollEvents = {};
auto* eventEventFd = &epollEvents[0];
auto* eventWpa = &epollEvents[1];

// Create an epoll instance to listen for events on the eventfd file descriptor and WPA control socket.
int ret{ -1 };
int fdEpoll = epoll_create1(0);
if (fdEpoll < 0) {
ret = errno;
LOGE << std::format("Failed to create epoll instance for interface '{}': {}", InterfaceName, ret);
return;
}

auto closeEpollFileDescriptorOnExit = notstd::ScopeExit([&] {
close(fdEpoll);
});

// Configure eventfd descriptor for thread stop signaling via epoll.
int fdEventFdStop = eventfd(0, 0);
if (fdEventFdStop < 0) {
ret = errno;
LOGE << std::format("Failed to create eventfd for interface '{}': {}", InterfaceName, ret);
return;
}

m_fdEventFdStop = fdEventFdStop;

auto closeEventFdStopFileDescriptorOnExit = notstd::ScopeExit([this] {
close(m_fdEventFdStop);
m_fdEventFdStop = -1;
});

eventEventFd->events = EPOLLIN;
eventEventFd->data.fd = fdEventFdStop;

// Add the eventfd descriptor to the epoll instance.
ret = epoll_ctl(fdEpoll, EPOLL_CTL_ADD, fdEventFdStop, eventEventFd);
if (ret < 0) {
ret = errno;
LOGE << std::format("Failed to add eventfd to epoll instance for interface '{}': {}", InterfaceName, ret);
return;
}

struct wpa_ctrl* wpaControlSocket = wpaControlSocketConnection;

// Configure WPA control socket for event signaling via epoll.
int fdWpa = wpa_ctrl_get_fd(wpaControlSocket);
if (fdWpa < 0) {
ret = errno;
LOGE << std::format("Failed to get WPA control socket file descriptor for interface '{}': {}", InterfaceName, ret);
return;
}

eventWpa->events = EPOLLIN;
eventWpa->data.fd = fdWpa;

ret = epoll_ctl(fdEpoll, EPOLL_CTL_ADD, fdWpa, eventWpa);
if (ret < 0) {
ret = errno;
LOGE << std::format("Failed to add WPA control socket to epoll instance for interface '{}': {}", InterfaceName, ret);
return;
}

// Attach to WPA event stream.
ret = wpa_ctrl_attach(wpaControlSocket);
if (ret < 0) {
ret = errno;
LOGE << std::format("Failed to attach to WPA control socket for interface '{}': {}", InterfaceName, ret);
return;
}

auto detachFromWpaControlSocketOnExit = notstd::ScopeExit([&] {
wpa_ctrl_detach(wpaControlSocket);
});

bool stopRequested{ false };
for (;;) {
if (stopToken.stop_requested() || stopRequested) {
LOGD << std::format("Stopping WPA event listener for interface '{}'.", InterfaceName);
break;
}

// Wait for at least one event file descriptor to be signaled.
const auto numEvents = epoll_wait(fdEpoll, std::data(epollEvents), std::size(epollEvents), -1);
if (numEvents < 0) {
ret = errno;
LOGE << std::format("Failed to wait for events on interface '{}': {}", InterfaceName, ret);
continue;
}
if (numEvents == 0) {
LOGW << std::format("No events received on interface '{}'.", InterfaceName);
continue;
}

// Determine which file descriptor was signaled, and handle it.
for (std::size_t i = 0; i < std::size(epollEvents); ++i) {
if (epollEvents[i].data.fd == fdEventFdStop) {
LOGD << "Eventfd was signaled; checking for pending event(s).";
uint64_t value{ ~EventFdStopRequestValue };
ssize_t numRead = read(fdEventFdStop, &value, sizeof value);
if (numRead != sizeof value) {
ret = errno;
LOGE << std::format("Failed to read from eventfd for interface '{}': numRead {} ({} {})", InterfaceName, numRead, ret, strerror(ret));
continue;
}
if (value == EventFdStopRequestValue) {
LOGD << std::format("Received stop signal on interface '{}'.", InterfaceName);
stopRequested = true;
break;
}
}
if (epollEvents[i].data.fd == fdWpa) {
LOGD << "WPA control socket was signaled; checking for pending event(s).";
while (wpa_ctrl_pending(wpaControlSocket) > 0) {
ProcessNextEvent(wpaControlSocketConnection);
}
}
}
}
}
Loading

0 comments on commit 262aec2

Please sign in to comment.