blob: 7644264d4d63eb5e54c75153d6b80eaeae54bfcb [file] [log] [blame]
/*
* Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
*
* Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
*
*/
#include <netlink/route/link.h>
#include <netlink/route/neighbour.h>
#include <utility>
#include "Rtnetlink.h"
#include "utils/log.h"
using namespace std::chrono_literals;
using namespace std::string_literals;
namespace {
template <class T>
void nlCacheForeachWrapper(nl_cache* cache, std::function<void(T*)> cb)
{
nl_cache_foreach(
cache, [](nl_object* obj, void* data) {
auto& cb = *static_cast<std::function<void(T*)>*>(data);
auto link = reinterpret_cast<T*>(obj);
cb(link);
},
&cb);
}
void nlCacheMngrCallbackWrapper(struct nl_cache*, struct nl_object* obj, int action, void* data)
{
auto objType = nl_object_get_type(obj);
if (objType == "route/link"s) {
auto* cb = static_cast<velia::system::Rtnetlink::LinkCB*>(data);
(*cb)(reinterpret_cast<rtnl_link*>(obj), action);
} else if (objType == "route/addr"s) {
auto* cb = static_cast<velia::system::Rtnetlink::AddrCB*>(data);
(*cb)(reinterpret_cast<rtnl_addr*>(obj), action);
} else if (objType == "route/route"s) {
auto* cb = static_cast<velia::system::Rtnetlink::RouteCB*>(data);
(*cb)(reinterpret_cast<rtnl_route*>(obj), action);
} else {
throw velia::system::RtnetlinkException("Unknown netlink object type in cache: '"s + objType + "'");
}
}
/** @brief Wraps rtnl object with unique_ptr and adds an appropriate deleter. */
template <class T>
std::unique_ptr<T, std::function<void(T*)>> nlObjectWrap(T* obj)
{
return std::unique_ptr<T, std::function<void(T*)>>(obj, [] (T* obj) { nl_object_put(OBJ_CAST(obj)); });
}
template <class T>
T* nlObjectClone(T* obj)
{
return reinterpret_cast<T*>(nl_object_clone(OBJ_CAST(obj)));
}
}
namespace velia::system {
namespace impl {
/** @brief Background thread watching for changes in netlink cache. Executes change callback from nl_cache_mngr_add. */
class nlCacheMngrWatcher {
std::atomic<bool> m_terminate;
std::thread m_thr;
static constexpr auto FD_POLL_INTERVAL = 500ms;
void run(velia::system::Rtnetlink::nlCacheManager manager);
public:
nlCacheMngrWatcher(velia::system::Rtnetlink::nlCacheManager manager);
~nlCacheMngrWatcher();
};
nlCacheMngrWatcher::nlCacheMngrWatcher(velia::system::Rtnetlink::nlCacheManager manager)
: m_terminate(false)
, m_thr(&nlCacheMngrWatcher::run, this, manager)
{
}
void nlCacheMngrWatcher::run(velia::system::Rtnetlink::nlCacheManager manager)
{
while (!m_terminate) {
if (auto err = nl_cache_mngr_poll(manager.get(), std::chrono::duration_cast<std::chrono::milliseconds>(FD_POLL_INTERVAL).count()); err < 0) {
throw velia::system::RtnetlinkException("nl_cache_mngr_poll", err);
}
}
}
nlCacheMngrWatcher::~nlCacheMngrWatcher()
{
m_terminate = true;
m_thr.join();
}
}
RtnetlinkException::RtnetlinkException(const std::string& msg)
: std::runtime_error("Rtnetlink exception: " + msg)
{
}
RtnetlinkException::RtnetlinkException(const std::string& funcName, int error)
: RtnetlinkException("Function '" + funcName + "' failed: " + nl_geterror(error))
{
}
Rtnetlink::Rtnetlink(LinkCB cbLink, AddrCB cbAddr, RouteCB cbRoute)
: m_log(spdlog::get("system"))
, m_nlSocket(nl_socket_alloc(), nl_socket_free)
, m_cbLink(std::move(cbLink))
, m_cbAddr(std::move(cbAddr))
, m_cbRoute(std::move(cbRoute))
{
if (!m_nlSocket) {
throw RtnetlinkException("nl_socket_alloc failed");
}
if (auto err = nl_connect(m_nlSocket.get(), NETLINK_ROUTE); err < 0) {
throw RtnetlinkException("nl_connect", err);
}
{
nl_cache_mngr* tmpManager;
if (auto err = nl_cache_mngr_alloc(nullptr /* alloc and manage new netlink socket */, NETLINK_ROUTE, NL_AUTO_PROVIDE, &tmpManager); err < 0) {
throw RtnetlinkException("nl_cache_mngr_alloc", err);
}
m_nlCacheManager = nlCacheManager(tmpManager, nl_cache_mngr_free);
}
// start listening for changes in cache manager in background thread
// FIXME: implement event loop instead of nlCacheMngrWatcher, maybe with https://www.freedesktop.org/software/systemd/man/sd-event.html
m_nlCacheMngrWatcher = std::make_unique<impl::nlCacheMngrWatcher>(m_nlCacheManager);
if (auto err = nl_cache_mngr_add(m_nlCacheManager.get(), "route/link", nlCacheMngrCallbackWrapper, &m_cbLink, &m_nlManagedCacheLink); err < 0) {
throw RtnetlinkException("nl_cache_mngr_add", err);
}
if (auto err = nl_cache_mngr_add(m_nlCacheManager.get(), "route/addr", nlCacheMngrCallbackWrapper, &m_cbAddr, &m_nlManagedCacheAddr); err < 0) {
throw RtnetlinkException("nl_cache_mngr_add", err);
}
if (auto err = nl_cache_mngr_add(m_nlCacheManager.get(), "route/route", nlCacheMngrCallbackWrapper, &m_cbRoute, &m_nlManagedCacheRoute); err < 0) {
throw RtnetlinkException("nl_cache_mngr_add", err);
}
{
nl_cache* tmpCache;
if (auto err = rtnl_link_alloc_cache(m_nlSocket.get(), AF_UNSPEC, &tmpCache); err < 0) {
throw RtnetlinkException("rtnl_link_alloc_cache", err);
}
m_nlCacheLink = nlCache(tmpCache, nl_cache_free);
}
{
nl_cache* tmpCache;
if (auto err = rtnl_neigh_alloc_cache(m_nlSocket.get(), &tmpCache); err < 0) {
throw RtnetlinkException("rtnl_neigh_alloc_cache", err);
}
m_nlCacheNeighbour = nlCache(tmpCache, nl_cache_free);
}
{
nl_cache* tmpCache;
if (auto err = rtnl_route_alloc_cache(m_nlSocket.get(), AF_UNSPEC, ROUTE_CACHE_CONTENT, &tmpCache); err < 0) {
throw RtnetlinkException("rtnl_route_alloc_cache", err);
}
m_nlCacheRoute = nlCache(tmpCache, nl_cache_free);
}
}
Rtnetlink::~Rtnetlink() = default;
/* @brief Fire callbacks after getting the initial data into the cache; populating the cache with nl_cache_mngr_add doesn't fire any cache change events
*
* This code can't be in constructor because the callbacks can invoke other Rtnetlink methods while the instance is not yet constructed.
*/
void Rtnetlink::invokeInitialCallbacks()
{
nlCacheForeachWrapper<rtnl_link>(m_nlManagedCacheLink, [this](rtnl_link* link) {
m_cbLink(link, NL_ACT_NEW);
});
nlCacheForeachWrapper<rtnl_addr>(m_nlManagedCacheAddr, [this](rtnl_addr* addr) {
m_cbAddr(addr, NL_ACT_NEW);
});
m_cbRoute(nullptr, NL_ACT_NEW); // when a single route is changed, we fetch all anyway, so no need to call update on all routes (as of now)
}
std::vector<Rtnetlink::nlLink> Rtnetlink::getLinks()
{
resyncCache(m_nlCacheLink);
std::vector<Rtnetlink::nlLink> res;
nlCacheForeachWrapper<rtnl_link>(m_nlCacheLink.get(), [&res](rtnl_link* link) {
res.emplace_back(nlObjectWrap(nlObjectClone(link)));
});
return res;
}
std::vector<std::pair<Rtnetlink::nlNeigh, Rtnetlink::nlLink>> Rtnetlink::getNeighbours()
{
resyncCache(m_nlCacheLink);
resyncCache(m_nlCacheNeighbour);
std::vector<std::pair<Rtnetlink::nlNeigh, Rtnetlink::nlLink>> res;
nlCacheForeachWrapper<rtnl_neigh>(m_nlCacheNeighbour.get(), [this, &res](rtnl_neigh* neigh) {
auto link = rtnl_link_get(m_nlCacheLink.get(), rtnl_neigh_get_ifindex(neigh));
res.emplace_back(nlObjectWrap(nlObjectClone(neigh)), nlObjectWrap(link));
});
return res;
}
std::vector<Rtnetlink::nlRoute> Rtnetlink::getRoutes()
{
#if 0
resyncCache(m_nlCacheRoute); // first resync lowers the number of routes. It doesn't seem to recognize one particular route in ipv6. Possibly related to https://github.com/thom311/libnl/issues/224?
#else
nl_cache* tmpCache;
if (auto err = rtnl_route_alloc_cache(m_nlSocket.get(), AF_UNSPEC, 0, &tmpCache); err < 0) {
throw RtnetlinkException("rtnl_route_alloc_cache", err);
}
m_nlCacheRoute = nlCache(tmpCache, nl_cache_free);
#endif
std::vector<Rtnetlink::nlRoute> res;
nlCacheForeachWrapper<rtnl_route>(m_nlCacheRoute.get(), [&res](rtnl_route* route) {
res.emplace_back(nlObjectWrap(nlObjectClone(route)));
});
return res;
}
void Rtnetlink::resyncCache(const nlCache& cache)
{
nl_cache_resync(m_nlSocket.get(), cache.get(), [](nl_cache*, nl_object*, int, void*){}, nullptr);
}
}