neighbor: Refactor out netlink code
This will allow us to re-use the generic netlink bits for other netlink
request operations.
Adds test coverage to the netlink message parsing routines for sanity
and regression prevention.
Tested:
Neighbor population still works when static neighbors are created on
the BMC.
Change-Id: I755e86eb76a8f6f825616c13279328134de87da9
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/Makefile.am b/Makefile.am
index 627bcbf..79db184 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -17,6 +17,7 @@
noinst_HEADERS = \
ethernet_interface.hpp \
neighbor.hpp \
+ netlink.hpp \
network_config.hpp \
network_manager.hpp \
ipaddress.hpp \
@@ -49,6 +50,7 @@
ethernet_interface.cpp \
neighbor.cpp \
ipaddress.cpp \
+ netlink.cpp \
network_config.cpp \
network_manager.cpp \
network_manager_main.cpp \
diff --git a/neighbor.cpp b/neighbor.cpp
index f7e6f05..1961ad3 100644
--- a/neighbor.cpp
+++ b/neighbor.cpp
@@ -3,6 +3,7 @@
#include "neighbor.hpp"
#include "ethernet_interface.hpp"
+#include "netlink.hpp"
#include "util.hpp"
#include <linux/neighbour.h>
@@ -16,205 +17,65 @@
#include <stdexcept>
#include <string_view>
#include <system_error>
+#include <vector>
namespace phosphor
{
namespace network
{
-
-NeighborInfo parseNeighbor(std::string_view msg)
+namespace detail
{
- struct ndmsg ndm;
- if (msg.size() < sizeof(ndm))
- {
- throw std::runtime_error("Bad neighbor msg");
- }
- memcpy(&ndm, msg.data(), sizeof(ndm));
- auto attrs = msg.substr(sizeof(ndm));
- NeighborInfo info;
- info.interface.resize(IF_NAMESIZE);
- if (if_indextoname(ndm.ndm_ifindex, info.interface.data()) == nullptr)
+void parseNeighbor(const nlmsghdr& hdr, std::string_view msg,
+ std::vector<NeighborInfo>& neighbors)
+{
+ if (hdr.nlmsg_type != RTM_NEWNEIGH)
+ {
+ throw std::runtime_error("Not a neighbor msg");
+ }
+ auto ndm = extract<ndmsg>(msg, "Bad neighbor msg");
+
+ NeighborInfo neighbor;
+ neighbor.interface.resize(IF_NAMESIZE);
+ if (if_indextoname(ndm.ndm_ifindex, neighbor.interface.data()) == nullptr)
{
throw std::system_error(errno, std::generic_category(),
"if_indextoname");
}
- info.interface.resize(strlen(info.interface.c_str()));
- info.permanent = ndm.ndm_state & NUD_PERMANENT;
+ neighbor.interface.resize(strlen(neighbor.interface.c_str()));
+ neighbor.permanent = ndm.ndm_state & NUD_PERMANENT;
bool set_addr = false;
- while (!attrs.empty())
+ while (!msg.empty())
{
- struct rtattr hdr;
- if (attrs.size() < sizeof(hdr))
- {
- throw std::runtime_error("Bad rtattr header");
- }
- memcpy(&hdr, attrs.data(), sizeof(hdr));
- if (hdr.rta_len < sizeof(hdr))
- {
- throw std::runtime_error("Invalid rtattr length");
- }
- if (attrs.size() < hdr.rta_len)
- {
- throw std::runtime_error("Not enough data for rtattr");
- }
- auto data = attrs.substr(RTA_LENGTH(0), hdr.rta_len - RTA_LENGTH(0));
+ auto [hdr, data] = netlink::extractRtAttr(msg);
if (hdr.rta_type == NDA_LLADDR)
{
- info.mac = mac_address::fromBuf(data);
+ neighbor.mac = mac_address::fromBuf(data);
}
else if (hdr.rta_type == NDA_DST)
{
- info.address = addrFromBuf(ndm.ndm_family, data);
+ neighbor.address = addrFromBuf(ndm.ndm_family, data);
set_addr = true;
}
- attrs.remove_prefix(RTA_ALIGN(hdr.rta_len));
}
if (!set_addr)
{
throw std::runtime_error("Missing address");
}
- return info;
+ neighbors.push_back(std::move(neighbor));
}
-bool parseNeighborMsgs(std::string_view msgs, std::vector<NeighborInfo>& info)
-{
- while (!msgs.empty())
- {
- struct nlmsghdr hdr;
- if (msgs.size() < sizeof(hdr))
- {
- throw std::runtime_error("Bad neighbor netlink header");
- }
- memcpy(&hdr, msgs.data(), sizeof(hdr));
- if (hdr.nlmsg_type == NLMSG_DONE)
- {
- if (msgs.size() > hdr.nlmsg_len)
- {
- throw std::runtime_error("Unexpected extra netlink messages");
- }
- return true;
- }
- else if (hdr.nlmsg_type != RTM_NEWNEIGH)
- {
- throw std::runtime_error("Bad neighbor msg type");
- }
- if (hdr.nlmsg_len < sizeof(hdr))
- {
- throw std::runtime_error("Invalid nlmsg length");
- }
- if (msgs.size() < hdr.nlmsg_len)
- {
- throw std::runtime_error("Bad neighbor payload");
- }
- auto msg = msgs.substr(NLMSG_HDRLEN, hdr.nlmsg_len - NLMSG_HDRLEN);
- msgs.remove_prefix(NLMSG_ALIGN(hdr.nlmsg_len));
- info.push_back(parseNeighbor(msg));
- }
-
- return false;
-}
-
-std::vector<NeighborInfo> receiveNeighbors(int sock)
-{
- // We need to make sure we have enough room for an entire packet otherwise
- // it gets truncated. The netlink docs guarantee packets will not exceed 8K
- char buf[8192];
-
- struct iovec iov;
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = buf;
- iov.iov_len = sizeof(buf);
-
- struct sockaddr_nl from;
- memset(&from, 0, sizeof(from));
- from.nl_family = AF_NETLINK;
-
- struct msghdr hdr;
- memset(&hdr, 0, sizeof(hdr));
- hdr.msg_name = &from;
- hdr.msg_namelen = sizeof(from);
- hdr.msg_iov = &iov;
- hdr.msg_iovlen = 1;
-
- std::vector<NeighborInfo> info;
- while (true)
- {
- ssize_t recvd = recvmsg(sock, &hdr, 0);
- if (recvd <= 0)
- {
- throw std::system_error(errno, std::generic_category(),
- "recvmsg neighbor");
- }
- if (parseNeighborMsgs(std::string_view(buf, recvd), info))
- {
- return info;
- }
- }
-}
-
-void requestNeighbors(int sock)
-{
- struct sockaddr_nl dst;
- memset(&dst, 0, sizeof(dst));
- dst.nl_family = AF_NETLINK;
-
- struct
- {
- struct nlmsghdr hdr;
- struct ndmsg msg;
- } data;
- memset(&data, 0, sizeof(data));
- data.hdr.nlmsg_len = sizeof(data);
- data.hdr.nlmsg_type = RTM_GETNEIGH;
- data.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
- data.msg.ndm_family = AF_UNSPEC;
-
- struct iovec iov;
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = &data;
- iov.iov_len = sizeof(data);
-
- struct msghdr hdr;
- memset(&hdr, 0, sizeof(hdr));
- hdr.msg_name = reinterpret_cast<struct sockaddr*>(&dst);
- hdr.msg_namelen = sizeof(dst);
- hdr.msg_iov = &iov;
- hdr.msg_iovlen = 1;
-
- if (sendmsg(sock, &hdr, 0) < 0)
- {
- throw std::system_error(errno, std::generic_category(),
- "sendmsg neighbor dump");
- }
-}
-
-int getNetlink(int protocol)
-{
- int sock = socket(AF_NETLINK, SOCK_DGRAM, protocol);
- if (sock < 0)
- {
- throw std::system_error(errno, std::generic_category(), "netlink open");
- }
-
- struct sockaddr_nl local;
- memset(&local, 0, sizeof(local));
- local.nl_family = AF_NETLINK;
- int r =
- bind(sock, reinterpret_cast<struct sockaddr*>(&local), sizeof(local));
- if (r < 0)
- {
- close(sock);
- throw std::system_error(errno, std::generic_category(), "netlink bind");
- }
- return sock;
-}
+} // namespace detail
std::vector<NeighborInfo> getCurrentNeighbors()
{
- Descriptor netlink(getNetlink(NETLINK_ROUTE));
- requestNeighbors(netlink());
- return receiveNeighbors(netlink());
+ std::vector<NeighborInfo> neighbors;
+ auto cb = [&neighbors](const nlmsghdr& hdr, std::string_view msg) {
+ detail::parseNeighbor(hdr, msg, neighbors);
+ };
+ netlink::performRequest(NETLINK_ROUTE, RTM_GETNEIGH, NLM_F_DUMP, ndmsg{},
+ cb);
+ return neighbors;
}
Neighbor::Neighbor(sdbusplus::bus::bus& bus, const char* objPath,
diff --git a/neighbor.hpp b/neighbor.hpp
index e103d95..074cae0 100644
--- a/neighbor.hpp
+++ b/neighbor.hpp
@@ -3,12 +3,13 @@
#include "types.hpp"
#include "util.hpp"
-#include <netinet/in.h>
+#include <linux/netlink.h>
#include <optional>
#include <sdbusplus/bus.hpp>
#include <sdbusplus/server/object.hpp>
#include <string>
+#include <string_view>
#include <vector>
#include <xyz/openbmc_project/Network/Neighbor/server.hpp>
#include <xyz/openbmc_project/Object/Delete/server.hpp>
@@ -78,5 +79,13 @@
EthernetInterface& parent;
};
+namespace detail
+{
+
+void parseNeighbor(const nlmsghdr& hdr, std::string_view msg,
+ std::vector<NeighborInfo>& neighbors);
+
+} // namespace detail
+
} // namespace network
} // namespace phosphor
diff --git a/netlink.cpp b/netlink.cpp
new file mode 100644
index 0000000..947b828
--- /dev/null
+++ b/netlink.cpp
@@ -0,0 +1,194 @@
+#include "netlink.hpp"
+
+#include "util.hpp"
+
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <array>
+#include <stdexcept>
+#include <system_error>
+
+namespace phosphor
+{
+namespace network
+{
+namespace netlink
+{
+namespace detail
+{
+
+void processMsg(std::string_view& msgs, bool& done, const ReceiveCallback& cb)
+{
+ // Parse and update the message buffer
+ auto hdr = copyFrom<nlmsghdr>(msgs, "Bad netlink header");
+ if (hdr.nlmsg_len < sizeof(hdr))
+ {
+ throw std::runtime_error("Invalid nlmsg length");
+ }
+ if (msgs.size() < hdr.nlmsg_len)
+ {
+ throw std::runtime_error("Bad nlmsg payload");
+ }
+ auto msg = msgs.substr(NLMSG_HDRLEN, hdr.nlmsg_len - NLMSG_HDRLEN);
+ msgs.remove_prefix(NLMSG_ALIGN(hdr.nlmsg_len));
+
+ // Figure out how to handle the individual message
+ bool doCallback = true;
+ if (hdr.nlmsg_flags & NLM_F_MULTI)
+ {
+ done = false;
+ }
+ if (hdr.nlmsg_type == NLMSG_NOOP)
+ {
+ doCallback = false;
+ }
+ else if (hdr.nlmsg_type == NLMSG_DONE)
+ {
+ if (done)
+ {
+ throw std::runtime_error("Got done for non-multi msg");
+ }
+ done = true;
+ doCallback = false;
+ }
+ else if (hdr.nlmsg_type == NLMSG_ERROR)
+ {
+ auto err = copyFrom<nlmsgerr>(msg, "Bad netlink error");
+ // This is just an ACK so don't do the callback
+ if (err.error <= 0)
+ {
+ doCallback = false;
+ }
+ }
+ // All multi-msg headers must have the multi flag
+ if (!done && !(hdr.nlmsg_flags & NLM_F_MULTI))
+ {
+ throw std::runtime_error("Got non-multi msg before done");
+ }
+ if (doCallback)
+ {
+ cb(hdr, msg);
+ }
+}
+
+static void receive(int sock, const ReceiveCallback& cb)
+{
+ // We need to make sure we have enough room for an entire packet otherwise
+ // it gets truncated. The netlink docs guarantee packets will not exceed 8K
+ std::array<char, 8192> buf;
+
+ iovec iov{};
+ iov.iov_base = buf.data();
+ iov.iov_len = buf.size();
+
+ sockaddr_nl from{};
+ from.nl_family = AF_NETLINK;
+
+ msghdr hdr{};
+ hdr.msg_name = &from;
+ hdr.msg_namelen = sizeof(from);
+ hdr.msg_iov = &iov;
+ hdr.msg_iovlen = 1;
+
+ // We only do multiple recvs if we have a MULTI type message
+ bool done = true;
+ do
+ {
+ ssize_t recvd = recvmsg(sock, &hdr, 0);
+ if (recvd < 0)
+ {
+ throw std::system_error(errno, std::generic_category(),
+ "netlink recvmsg");
+ }
+ if (recvd == 0)
+ {
+ throw std::runtime_error("netlink recvmsg: Got empty payload");
+ }
+
+ std::string_view msgs(buf.data(), recvd);
+ do
+ {
+ processMsg(msgs, done, cb);
+ } while (!done && !msgs.empty());
+
+ if (done && !msgs.empty())
+ {
+ throw std::runtime_error("Extra unprocessed netlink messages");
+ }
+ } while (!done);
+}
+
+static void requestSend(int sock, void* data, size_t size)
+{
+ sockaddr_nl dst{};
+ dst.nl_family = AF_NETLINK;
+
+ iovec iov{};
+ iov.iov_base = data;
+ iov.iov_len = size;
+
+ msghdr hdr{};
+ hdr.msg_name = reinterpret_cast<sockaddr*>(&dst);
+ hdr.msg_namelen = sizeof(dst);
+ hdr.msg_iov = &iov;
+ hdr.msg_iovlen = 1;
+
+ if (sendmsg(sock, &hdr, 0) < 0)
+ {
+ throw std::system_error(errno, std::generic_category(),
+ "netlink sendmsg");
+ }
+}
+
+static int newRequestSocket(int protocol)
+{
+ int sock = socket(AF_NETLINK, SOCK_RAW, protocol);
+ if (sock < 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "netlink open");
+ }
+
+ sockaddr_nl local{};
+ local.nl_family = AF_NETLINK;
+ int r = bind(sock, reinterpret_cast<sockaddr*>(&local), sizeof(local));
+ if (r < 0)
+ {
+ close(sock);
+ throw std::system_error(errno, std::generic_category(), "netlink bind");
+ }
+
+ return sock;
+}
+
+void performRequest(int protocol, void* data, size_t size,
+ const ReceiveCallback& cb)
+{
+ Descriptor sock(newRequestSocket(protocol));
+ requestSend(sock(), data, size);
+ receive(sock(), cb);
+}
+
+} // namespace detail
+
+std::tuple<rtattr, std::string_view> extractRtAttr(std::string_view& data)
+{
+ auto hdr = copyFrom<rtattr>(data, "Bad rtattr header");
+ if (hdr.rta_len < RTA_LENGTH(0))
+ {
+ throw std::runtime_error("Invalid rtattr length");
+ }
+ if (data.size() < hdr.rta_len)
+ {
+ throw std::runtime_error("Not enough data for rtattr");
+ }
+ auto attr = data.substr(RTA_LENGTH(0), hdr.rta_len - RTA_LENGTH(0));
+ data.remove_prefix(RTA_ALIGN(hdr.rta_len));
+ return {hdr, attr};
+}
+
+} // namespace netlink
+} // namespace network
+} // namespace phosphor
diff --git a/netlink.hpp b/netlink.hpp
new file mode 100644
index 0000000..e1a8253
--- /dev/null
+++ b/netlink.hpp
@@ -0,0 +1,69 @@
+#pragma once
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+
+#include <functional>
+#include <string_view>
+#include <tuple>
+#include <type_traits>
+
+namespace phosphor
+{
+namespace network
+{
+namespace netlink
+{
+
+/* @brief Called on each nlmsg received on the socket
+ */
+using ReceiveCallback = std::function<void(const nlmsghdr&, std::string_view)>;
+
+namespace detail
+{
+
+void processMsg(std::string_view& msgs, bool& done, const ReceiveCallback& cb);
+
+void performRequest(int protocol, void* data, size_t size,
+ const ReceiveCallback& cb);
+
+} // namespace detail
+
+/* @brief Call on a block of rtattrs to parse a single one out
+ * Updates the input to remove the attr parsed out.
+ *
+ * @param[in,out] attrs - The buffer holding rtattrs to parse
+ * @return A tuple of rtattr header + data buffer for the attr
+ */
+std::tuple<rtattr, std::string_view> extractRtAttr(std::string_view& data);
+
+/** @brief Performs a netlink request of the specified type with the given
+ * message Calls the callback upon receiving
+ *
+ * @param[in] protocol - The netlink protocol to use when opening the socket
+ * @param[in] type - The netlink message type
+ * @param[in] flags - Additional netlink flags for the request
+ * @param[in] msg - The message payload for the request
+ * @param[in] cb - Called for each response message payload
+ */
+template <typename T>
+void performRequest(int protocol, uint16_t type, uint16_t flags, const T& msg,
+ const ReceiveCallback& cb)
+{
+ static_assert(std::is_trivially_copyable_v<T>);
+
+ struct
+ {
+ nlmsghdr hdr;
+ T msg;
+ } data{};
+ data.hdr.nlmsg_len = sizeof(data);
+ data.hdr.nlmsg_type = type;
+ data.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | flags;
+ data.msg = msg;
+
+ detail::performRequest(protocol, &data, sizeof(data), cb);
+}
+
+} // namespace netlink
+} // namespace network
+} // namespace phosphor
diff --git a/test/Makefile.am b/test/Makefile.am
index ffa4655..388f679 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -7,6 +7,8 @@
test_SOURCES = \
test_util.cpp \
mock_syscall.cpp \
+ test_neighbor.cpp \
+ test_netlink.cpp \
test_network_manager.cpp \
test_ethernet_interface.cpp \
test_rtnetlink.cpp \
@@ -50,6 +52,7 @@
$(top_builddir)/network_config.o \
$(top_builddir)/ipaddress.o \
$(top_builddir)/neighbor.o \
+ $(top_builddir)/netlink.o \
$(top_builddir)/routing_table.o \
$(top_builddir)/util.o \
$(top_builddir)/rtnetlink_server.o \
diff --git a/test/test_neighbor.cpp b/test/test_neighbor.cpp
new file mode 100644
index 0000000..a76a7d5
--- /dev/null
+++ b/test/test_neighbor.cpp
@@ -0,0 +1,173 @@
+#include "neighbor.hpp"
+#include "util.hpp"
+
+#include <arpa/inet.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <net/ethernet.h>
+#include <net/if.h>
+
+#include <cstring>
+#include <stdexcept>
+#include <string>
+#include <system_error>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace phosphor
+{
+namespace network
+{
+namespace detail
+{
+
+TEST(ParseNeighbor, NotNeighborType)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_type = RTM_NEWLINK;
+
+ std::vector<NeighborInfo> neighbors;
+ EXPECT_THROW(parseNeighbor(hdr, "", neighbors), std::runtime_error);
+ EXPECT_EQ(0, neighbors.size());
+}
+
+TEST(ParseNeighbor, SmallMsg)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_type = RTM_NEWNEIGH;
+ std::string data = "1";
+
+ std::vector<NeighborInfo> neighbors;
+ EXPECT_THROW(parseNeighbor(hdr, data, neighbors), std::runtime_error);
+ EXPECT_EQ(0, neighbors.size());
+}
+
+TEST(ParseNeighbor, BadIf)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_type = RTM_NEWNEIGH;
+ ndmsg msg{};
+ std::string data;
+ data.append(reinterpret_cast<char*>(&msg), sizeof(msg));
+
+ std::vector<NeighborInfo> neighbors;
+ EXPECT_THROW(parseNeighbor(hdr, data, neighbors), std::system_error);
+ EXPECT_EQ(0, neighbors.size());
+}
+
+TEST(ParseNeighbor, NoAttrs)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_type = RTM_NEWNEIGH;
+ ndmsg msg{};
+ msg.ndm_ifindex = if_nametoindex("lo");
+ ASSERT_NE(0, msg.ndm_ifindex);
+ std::string data;
+ data.append(reinterpret_cast<char*>(&msg), sizeof(msg));
+
+ std::vector<NeighborInfo> neighbors;
+ EXPECT_THROW(parseNeighbor(hdr, data, neighbors), std::runtime_error);
+ EXPECT_EQ(0, neighbors.size());
+}
+
+TEST(ParseNeighbor, NoAddress)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_type = RTM_NEWNEIGH;
+ ndmsg msg{};
+ msg.ndm_ifindex = if_nametoindex("lo");
+ ASSERT_NE(0, msg.ndm_ifindex);
+ ether_addr mac = {{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa}};
+ rtattr lladdr{};
+ lladdr.rta_len = RTA_LENGTH(sizeof(mac));
+ lladdr.rta_type = NDA_LLADDR;
+ char lladdrbuf[RTA_ALIGN(lladdr.rta_len)];
+ std::memset(lladdrbuf, '\0', sizeof(lladdrbuf));
+ std::memcpy(lladdrbuf, &lladdr, sizeof(lladdr));
+ std::memcpy(RTA_DATA(lladdrbuf), &mac, sizeof(mac));
+ std::string data;
+ data.append(reinterpret_cast<char*>(&msg), sizeof(msg));
+ data.append(reinterpret_cast<char*>(&lladdrbuf), sizeof(lladdrbuf));
+
+ std::vector<NeighborInfo> neighbors;
+ EXPECT_THROW(parseNeighbor(hdr, data, neighbors), std::runtime_error);
+ EXPECT_EQ(0, neighbors.size());
+}
+
+TEST(ParseNeighbor, NoMAC)
+{
+ constexpr auto ifstr = "lo";
+ nlmsghdr hdr{};
+ hdr.nlmsg_type = RTM_NEWNEIGH;
+ ndmsg msg{};
+ msg.ndm_family = AF_INET;
+ msg.ndm_state = NUD_PERMANENT;
+ msg.ndm_ifindex = if_nametoindex(ifstr);
+ ASSERT_NE(0, msg.ndm_ifindex);
+ in_addr addr;
+ ASSERT_EQ(1, inet_pton(msg.ndm_family, "192.168.10.1", &addr));
+ rtattr dst{};
+ dst.rta_len = RTA_LENGTH(sizeof(addr));
+ dst.rta_type = NDA_DST;
+ char dstbuf[RTA_ALIGN(dst.rta_len)];
+ std::memset(dstbuf, '\0', sizeof(dstbuf));
+ std::memcpy(dstbuf, &dst, sizeof(dst));
+ std::memcpy(RTA_DATA(dstbuf), &addr, sizeof(addr));
+ std::string data;
+ data.append(reinterpret_cast<char*>(&msg), sizeof(msg));
+ data.append(reinterpret_cast<char*>(&dstbuf), sizeof(dstbuf));
+
+ std::vector<NeighborInfo> neighbors;
+ parseNeighbor(hdr, data, neighbors);
+ EXPECT_EQ(1, neighbors.size());
+ EXPECT_EQ(ifstr, neighbors[0].interface);
+ EXPECT_TRUE(neighbors[0].permanent);
+ EXPECT_FALSE(neighbors[0].mac);
+ EXPECT_TRUE(equal(addr, std::get<in_addr>(neighbors[0].address)));
+}
+
+TEST(ParseNeighbor, Full)
+{
+ constexpr auto ifstr = "lo";
+ nlmsghdr hdr{};
+ hdr.nlmsg_type = RTM_NEWNEIGH;
+ ndmsg msg{};
+ msg.ndm_family = AF_INET6;
+ msg.ndm_state = NUD_NOARP;
+ msg.ndm_ifindex = if_nametoindex(ifstr);
+ ether_addr mac = {{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa}};
+ rtattr lladdr{};
+ lladdr.rta_len = RTA_LENGTH(sizeof(mac));
+ lladdr.rta_type = NDA_LLADDR;
+ char lladdrbuf[RTA_ALIGN(lladdr.rta_len)];
+ std::memset(lladdrbuf, '\0', sizeof(lladdrbuf));
+ std::memcpy(lladdrbuf, &lladdr, sizeof(lladdr));
+ std::memcpy(RTA_DATA(lladdrbuf), &mac, sizeof(mac));
+ in6_addr addr;
+ ASSERT_EQ(1, inet_pton(msg.ndm_family, "fd00::1", &addr));
+ rtattr dst{};
+ dst.rta_len = RTA_LENGTH(sizeof(addr));
+ dst.rta_type = NDA_DST;
+ char dstbuf[RTA_ALIGN(dst.rta_len)];
+ std::memset(dstbuf, '\0', sizeof(dstbuf));
+ std::memcpy(dstbuf, &dst, sizeof(dst));
+ std::memcpy(RTA_DATA(dstbuf), &addr, sizeof(addr));
+ std::string data;
+ data.append(reinterpret_cast<char*>(&msg), sizeof(msg));
+ data.append(reinterpret_cast<char*>(&lladdrbuf), sizeof(lladdrbuf));
+ data.append(reinterpret_cast<char*>(&dstbuf), sizeof(dstbuf));
+
+ std::vector<NeighborInfo> neighbors;
+ parseNeighbor(hdr, data, neighbors);
+ EXPECT_EQ(1, neighbors.size());
+ EXPECT_EQ(ifstr, neighbors[0].interface);
+ EXPECT_FALSE(neighbors[0].permanent);
+ EXPECT_TRUE(neighbors[0].mac);
+ EXPECT_EQ(0, std::memcmp(&mac, neighbors[0].mac->data(), sizeof(mac)));
+ EXPECT_TRUE(equal(addr, std::get<in6_addr>(neighbors[0].address)));
+}
+
+} // namespace detail
+} // namespace network
+} // namespace phosphor
diff --git a/test/test_netlink.cpp b/test/test_netlink.cpp
new file mode 100644
index 0000000..b788dd1
--- /dev/null
+++ b/test/test_netlink.cpp
@@ -0,0 +1,290 @@
+#include "netlink.hpp"
+#include "util.hpp"
+
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+
+#include <cstring>
+#include <stdexcept>
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace phosphor
+{
+namespace network
+{
+namespace netlink
+{
+namespace detail
+{
+
+TEST(ExtractMsgs, TooSmall)
+{
+ const char buf[] = {'1'};
+ static_assert(sizeof(buf) < sizeof(nlmsghdr));
+ std::string_view data(buf, sizeof(buf));
+
+ size_t cbCalls = 0;
+ auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+ bool done = true;
+ EXPECT_THROW(processMsg(data, done, cb), std::runtime_error);
+ EXPECT_EQ(1, data.size());
+ EXPECT_EQ(0, cbCalls);
+ EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, SmallAttrLen)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_len = NLMSG_LENGTH(0) - 1;
+ std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+ size_t cbCalls = 0;
+ auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+ bool done = true;
+ EXPECT_THROW(processMsg(data, done, cb), std::runtime_error);
+ EXPECT_EQ(NLMSG_SPACE(0), data.size());
+ EXPECT_EQ(0, cbCalls);
+ EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, LargeAttrLen)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_len = NLMSG_LENGTH(0) + 1;
+ std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+ size_t cbCalls = 0;
+ auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+ bool done = true;
+ EXPECT_THROW(processMsg(data, done, cb), std::runtime_error);
+ EXPECT_EQ(NLMSG_SPACE(0), data.size());
+ EXPECT_EQ(0, cbCalls);
+ EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, NoopMsg)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_len = NLMSG_LENGTH(0);
+ hdr.nlmsg_type = NLMSG_NOOP;
+ std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+ size_t cbCalls = 0;
+ auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+ bool done = true;
+ processMsg(data, done, cb);
+ EXPECT_EQ(0, data.size());
+ EXPECT_EQ(0, cbCalls);
+ EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, AckMsg)
+{
+ nlmsgerr ack{};
+ nlmsghdr hdr{};
+ hdr.nlmsg_len = NLMSG_LENGTH(sizeof(ack));
+ hdr.nlmsg_type = NLMSG_ERROR;
+ char buf[NLMSG_ALIGN(hdr.nlmsg_len)];
+ std::memcpy(buf, &hdr, sizeof(hdr));
+ std::memcpy(NLMSG_DATA(buf), &ack, sizeof(ack));
+ std::string_view data(reinterpret_cast<char*>(&buf), sizeof(buf));
+
+ size_t cbCalls = 0;
+ auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+ bool done = true;
+ processMsg(data, done, cb);
+ EXPECT_EQ(0, data.size());
+ EXPECT_EQ(0, cbCalls);
+ EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, ErrMsg)
+{
+ nlmsgerr err{};
+ err.error = EINVAL;
+ nlmsghdr hdr{};
+ hdr.nlmsg_len = NLMSG_LENGTH(sizeof(err));
+ hdr.nlmsg_type = NLMSG_ERROR;
+ char buf[NLMSG_ALIGN(hdr.nlmsg_len)];
+ std::memcpy(buf, &hdr, sizeof(hdr));
+ std::memcpy(NLMSG_DATA(buf), &err, sizeof(err));
+ std::string_view data(reinterpret_cast<char*>(&buf), sizeof(buf));
+
+ size_t cbCalls = 0;
+ nlmsghdr hdrOut;
+ std::string_view dataOut;
+ auto cb = [&](const nlmsghdr& hdr, std::string_view data) {
+ hdrOut = hdr;
+ dataOut = data;
+ cbCalls++;
+ };
+ bool done = true;
+ processMsg(data, done, cb);
+ EXPECT_EQ(0, data.size());
+ EXPECT_EQ(1, cbCalls);
+ EXPECT_TRUE(equal(hdr, hdrOut));
+ EXPECT_TRUE(equal(err, extract<nlmsgerr>(dataOut)));
+ EXPECT_EQ(0, dataOut.size());
+ EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, DoneNoMulti)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_len = NLMSG_LENGTH(0);
+ hdr.nlmsg_type = NLMSG_DONE;
+ std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+ size_t cbCalls = 0;
+ auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+ bool done = true;
+ EXPECT_THROW(processMsg(data, done, cb), std::runtime_error);
+ EXPECT_EQ(0, data.size());
+ EXPECT_EQ(0, cbCalls);
+ EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsg, TwoMultiMsgs)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_len = NLMSG_LENGTH(0);
+ hdr.nlmsg_type = RTM_NEWLINK;
+ hdr.nlmsg_flags = NLM_F_MULTI;
+ std::string buf;
+ buf.append(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+ buf.append(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+ std::string_view data = buf;
+ size_t cbCalls = 0;
+ auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+ bool done = true;
+ processMsg(data, done, cb);
+ EXPECT_EQ(NLMSG_SPACE(0), data.size());
+ EXPECT_EQ(1, cbCalls);
+ EXPECT_FALSE(done);
+
+ processMsg(data, done, cb);
+ EXPECT_EQ(0, data.size());
+ EXPECT_EQ(2, cbCalls);
+ EXPECT_FALSE(done);
+}
+
+TEST(ExtractMsgs, MultiMsgValid)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_len = NLMSG_LENGTH(0);
+ hdr.nlmsg_type = RTM_NEWLINK;
+ hdr.nlmsg_flags = NLM_F_MULTI;
+ std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+ size_t cbCalls = 0;
+ auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+ bool done = true;
+ processMsg(data, done, cb);
+ EXPECT_EQ(0, data.size());
+ EXPECT_EQ(1, cbCalls);
+ EXPECT_FALSE(done);
+
+ hdr.nlmsg_type = NLMSG_DONE;
+ hdr.nlmsg_flags = 0;
+ data = std::string_view(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+ processMsg(data, done, cb);
+ EXPECT_EQ(0, data.size());
+ EXPECT_EQ(1, cbCalls);
+ EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, MultiMsgInvalid)
+{
+ nlmsghdr hdr{};
+ hdr.nlmsg_len = NLMSG_LENGTH(0);
+ hdr.nlmsg_type = RTM_NEWLINK;
+ hdr.nlmsg_flags = NLM_F_MULTI;
+ std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+ size_t cbCalls = 0;
+ auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+ bool done = true;
+ processMsg(data, done, cb);
+ EXPECT_EQ(0, data.size());
+ EXPECT_EQ(1, cbCalls);
+ EXPECT_FALSE(done);
+
+ hdr.nlmsg_flags = 0;
+ data = std::string_view(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+ EXPECT_THROW(processMsg(data, done, cb), std::runtime_error);
+ EXPECT_EQ(0, data.size());
+ EXPECT_EQ(1, cbCalls);
+ EXPECT_FALSE(done);
+}
+
+} // namespace detail
+
+TEST(ExtractRtAttr, TooSmall)
+{
+ const char buf[] = {'1'};
+ static_assert(sizeof(buf) < sizeof(rtattr));
+ std::string_view data(buf, sizeof(buf));
+
+ EXPECT_THROW(extractRtAttr(data), std::runtime_error);
+ EXPECT_EQ(1, data.size());
+}
+
+TEST(ExtractRtAttr, SmallAttrLen)
+{
+ rtattr rta{};
+ rta.rta_len = RTA_LENGTH(0) - 1;
+ std::string_view data(reinterpret_cast<char*>(&rta), RTA_SPACE(0));
+
+ EXPECT_THROW(extractRtAttr(data), std::runtime_error);
+ EXPECT_EQ(RTA_SPACE(0), data.size());
+}
+
+TEST(ExtractRtAttr, LargeAttrLen)
+{
+ rtattr rta{};
+ rta.rta_len = RTA_LENGTH(0) + 1;
+ std::string_view data(reinterpret_cast<char*>(&rta), RTA_SPACE(0));
+
+ EXPECT_THROW(extractRtAttr(data), std::runtime_error);
+ EXPECT_EQ(RTA_SPACE(0), data.size());
+}
+
+TEST(ExtractRtAttr, NoData)
+{
+ rtattr rta{};
+ rta.rta_len = RTA_LENGTH(0);
+ std::string_view data(reinterpret_cast<char*>(&rta), RTA_SPACE(0));
+
+ auto [hdr, attr] = extractRtAttr(data);
+ EXPECT_EQ(0, data.size());
+ EXPECT_EQ(0, attr.size());
+ EXPECT_EQ(0, std::memcmp(&rta, &hdr, sizeof(rta)));
+}
+
+TEST(ExtractRtAttr, SomeData)
+{
+ const char attrbuf[] = "abcd";
+ const char nextbuf[] = "efgh";
+ rtattr rta{};
+ rta.rta_len = RTA_LENGTH(sizeof(attrbuf));
+
+ char buf[RTA_SPACE(sizeof(attrbuf)) + sizeof(nextbuf)];
+ memcpy(buf, &rta, sizeof(rta));
+ memcpy(RTA_DATA(buf), &attrbuf, sizeof(attrbuf));
+ memcpy(buf + RTA_SPACE(sizeof(attrbuf)), &nextbuf, sizeof(nextbuf));
+ std::string_view data(buf, sizeof(buf));
+
+ auto [hdr, attr] = extractRtAttr(data);
+ EXPECT_EQ(0, memcmp(&rta, &hdr, sizeof(rta)));
+ EXPECT_EQ(sizeof(attrbuf), attr.size());
+ EXPECT_EQ(0, memcmp(&attrbuf, attr.data(), sizeof(attrbuf)));
+ EXPECT_EQ(sizeof(nextbuf), data.size());
+ EXPECT_EQ(0, memcmp(&nextbuf, data.data(), sizeof(nextbuf)));
+}
+
+} // namespace netlink
+} // namespace network
+} // namespace phosphor
diff --git a/test/test_util.cpp b/test/test_util.cpp
index 4e79394..3cf28e3 100644
--- a/test/test_util.cpp
+++ b/test/test_util.cpp
@@ -4,6 +4,9 @@
#include <netinet/in.h>
#include <cstddef>
+#include <cstring>
+#include <string>
+#include <string_view>
#include <xyz/openbmc_project/Common/error.hpp>
#include <gtest/gtest.h>
@@ -13,6 +16,7 @@
namespace network
{
+using namespace std::literals;
using InternalFailure =
sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure;
class TestUtil : public testing::Test
@@ -268,5 +272,93 @@
EXPECT_EQ("fe80::", address);
}
+TEST_F(TestUtil, CopyFromTooSmall)
+{
+ constexpr auto expected = "abcde"sv;
+ struct
+ {
+ uint8_t data[10];
+ } data;
+ static_assert(sizeof(data) > expected.size());
+ EXPECT_THROW(copyFrom<decltype(data)>(expected), std::runtime_error);
+}
+
+TEST_F(TestUtil, CopyFromSome)
+{
+ constexpr auto expected = "abcde"sv;
+ struct
+ {
+ uint8_t data[2];
+ } data;
+ static_assert(sizeof(data) < expected.size());
+ data = copyFrom<decltype(data)>(expected);
+ EXPECT_EQ(0, memcmp(&data, expected.data(), sizeof(data)));
+}
+
+TEST_F(TestUtil, CopyFromAll)
+{
+ constexpr auto expected = "abcde"sv;
+ struct
+ {
+ uint8_t data[5];
+ } data;
+ static_assert(sizeof(data) == expected.size());
+ data = copyFrom<decltype(data)>(expected);
+ EXPECT_EQ(0, memcmp(&data, expected.data(), sizeof(data)));
+}
+
+TEST_F(TestUtil, ExtractSome)
+{
+ constexpr auto expected = "abcde"sv;
+ auto buf = expected;
+ struct
+ {
+ uint8_t data[2];
+ } data;
+ static_assert(sizeof(data) < expected.size());
+ data = extract<decltype(data)>(buf);
+ EXPECT_EQ(0, memcmp(&data, expected.data(), sizeof(data)));
+ EXPECT_EQ(3, buf.size());
+ EXPECT_EQ(expected.substr(2), buf);
+}
+
+TEST_F(TestUtil, ExtractAll)
+{
+ constexpr auto expected = "abcde"sv;
+ auto buf = expected;
+ struct
+ {
+ uint8_t data[5];
+ } data;
+ static_assert(sizeof(data) == expected.size());
+ data = extract<decltype(data)>(buf);
+ EXPECT_EQ(0, memcmp(&data, expected.data(), sizeof(data)));
+ EXPECT_EQ(0, buf.size());
+}
+
+TEST_F(TestUtil, Equal)
+{
+ struct
+ {
+ int i;
+ } a, b{};
+ a.i = 4;
+ b.i = 4;
+
+ EXPECT_TRUE(equal(a, b));
+}
+
+TEST_F(TestUtil, NotEqual)
+{
+ struct
+ {
+ int i;
+ } a, b{};
+ a.i = 2;
+ b.i = 4;
+
+ EXPECT_FALSE(equal(a, b));
+}
+
} // namespace network
} // namespace phosphor
diff --git a/util.hpp b/util.hpp
index 723c941..fe94444 100644
--- a/util.hpp
+++ b/util.hpp
@@ -200,6 +200,53 @@
} // namespace network
+/** @brief Copies data from a buffer into a copyable type
+ *
+ * @param[in] data - The data buffer being extracted from
+ * @param[in] emsg - The message to print if extraction fails
+ * @return The copyable type with data populated
+ */
+template <typename T>
+T copyFrom(std::string_view data, const char* emsg = "Extract Failed")
+{
+ static_assert(std::is_trivially_copyable_v<T>);
+ T ret;
+ if (data.size() < sizeof(ret))
+ {
+ throw std::runtime_error(emsg);
+ }
+ std::memcpy(&ret, data.data(), sizeof(ret));
+ return ret;
+}
+
+/** @brief Extracts data from a buffer into a copyable type
+ * Updates the data buffer to show that data was removed
+ *
+ * @param[in,out] data - The data buffer being extracted from
+ * @param[in] emsg - The message to print if extraction fails
+ * @return The copyable type with data populated
+ */
+template <typename T>
+T extract(std::string_view& data, const char* emsg = "Extract Failed")
+{
+ T ret = copyFrom<T>(data, emsg);
+ data.remove_prefix(sizeof(ret));
+ return ret;
+}
+
+/** @brief Compares two of the same trivially copyable types
+ *
+ * @param[in] a - The data buffer being extracted from
+ * @param[in] b - The message to print if extraction fails
+ * @return True if the parameters are bitwise identical
+ */
+template <typename T>
+bool equal(const T& a, const T& b)
+{
+ static_assert(std::is_trivially_copyable_v<T>);
+ return memcmp(&a, &b, sizeof(T)) == 0;
+}
+
class Descriptor
{
private: