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: