Static neighbor support

This changes adds support for assigning static ARP / NDP
neighbors on each ethernet interface.

Tested:
    Ran inside a romulus VM and made sure that `ip neigh add`
    commands triggered the refresh of static_neighbor objects.
    Also verified that static neighbors could be added through
    the CreateStatic dbus interface. Verified that deleting those
    static entries would persist to the system.

Change-Id: Ifaf753525c532b6dade392d7eb4ebd6d7242d480
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/Makefile.am b/Makefile.am
index 726507e..16688dd 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,7 +1,8 @@
 # Build these headers, don't install them
 nobase_nodist_include_HEADERS = \
 	xyz/openbmc_project/Network/VLAN/Create/server.hpp \
-	xyz/openbmc_project/Network/IP/Create/server.hpp
+	xyz/openbmc_project/Network/IP/Create/server.hpp \
+	xyz/openbmc_project/Network/Neighbor/CreateStatic/server.hpp
 
 if HAVE_SYSTEMD
 systemdsystemunit_DATA = \
@@ -15,6 +16,7 @@
 
 noinst_HEADERS = \
 		ethernet_interface.hpp \
+		neighbor.hpp \
 		network_config.hpp \
 		network_manager.hpp \
 		ipaddress.hpp \
@@ -45,6 +47,7 @@
 
 phosphor_network_manager_SOURCES = \
 		ethernet_interface.cpp \
+		neighbor.cpp \
 		ipaddress.cpp \
 		network_config.cpp \
 		network_manager.cpp \
@@ -52,6 +55,7 @@
 		system_configuration.cpp \
 		xyz/openbmc_project/Network/VLAN/Create/server.cpp \
 		xyz/openbmc_project/Network/IP/Create/server.cpp \
+		xyz/openbmc_project/Network/Neighbor/CreateStatic/server.cpp \
 		util.cpp \
 		routing_table.cpp \
 		config_parser.cpp \
@@ -65,13 +69,17 @@
 		xyz/openbmc_project/Network/VLAN/Create/server.cpp \
 		xyz/openbmc_project/Network/VLAN/Create/server.hpp \
 		xyz/openbmc_project/Network/IP/Create/server.cpp \
-		xyz/openbmc_project/Network/IP/Create/server.hpp
+		xyz/openbmc_project/Network/IP/Create/server.hpp \
+		xyz/openbmc_project/Network/Neighbor/CreateStatic/server.cpp \
+		xyz/openbmc_project/Network/Neighbor/CreateStatic/server.hpp
 
 BUILT_SOURCES = \
 		xyz/openbmc_project/Network/VLAN/Create/server.cpp \
 		xyz/openbmc_project/Network/VLAN/Create/server.hpp \
 		xyz/openbmc_project/Network/IP/Create/server.cpp \
-		xyz/openbmc_project/Network/IP/Create/server.hpp
+		xyz/openbmc_project/Network/IP/Create/server.hpp \
+		xyz/openbmc_project/Network/Neighbor/CreateStatic/server.cpp \
+		xyz/openbmc_project/Network/Neighbor/CreateStatic/server.hpp
 
 phosphor_network_manager_LDFLAGS = \
 		$(SYSTEMD_LIBS) \
@@ -106,4 +114,13 @@
 	$(SDBUSPLUSPLUS) -r $(srcdir) interface server-header xyz.openbmc_project.Network.IP.Create > $@
 	sed -i '5i #include \"xyz\/openbmc_project\/Network\/IP\/server.hpp\"' $@
 
+xyz/openbmc_project/Network/Neighbor/CreateStatic/server.cpp: xyz/openbmc_project/Network/Neighbor/CreateStatic.interface.yaml xyz/openbmc_project/Network/Neighbor/CreateStatic/server.hpp
+	@mkdir -p `dirname $@`
+	$(SDBUSPLUSPLUS) -r $(srcdir) interface server-cpp xyz.openbmc_project.Network.Neighbor.CreateStatic > $@
+
+xyz/openbmc_project/Network/Neighbor/CreateStatic/server.hpp: xyz/openbmc_project/Network/Neighbor/CreateStatic.interface.yaml
+	@mkdir -p `dirname $@`
+	$(SDBUSPLUSPLUS) -r $(srcdir) interface server-header xyz.openbmc_project.Network.Neighbor.CreateStatic > $@
+	sed -i '5i #include \"xyz\/openbmc_project\/Network\/Neighbor\/server.hpp\"' $@
+
 SUBDIRS = test
diff --git a/ethernet_interface.cpp b/ethernet_interface.cpp
index 154efcb..92ba2cc 100644
--- a/ethernet_interface.cpp
+++ b/ethernet_interface.cpp
@@ -4,6 +4,7 @@
 
 #include "config_parser.hpp"
 #include "ipaddress.hpp"
+#include "neighbor.hpp"
 #include "network_manager.hpp"
 #include "routing_table.hpp"
 #include "vlan_interface.hpp"
@@ -104,6 +105,28 @@
     }
 }
 
+void EthernetInterface::createStaticNeighborObjects()
+{
+    staticNeighbors.clear();
+
+    auto neighbors = getCurrentNeighbors();
+    for (const auto& neighbor : neighbors)
+    {
+        if (!neighbor.permanent || !neighbor.mac ||
+            neighbor.interface != interfaceName())
+        {
+            continue;
+        }
+        std::string ip = toString(neighbor.address);
+        std::string mac = mac_address::toString(*neighbor.mac);
+        std::string objectPath = generateStaticNeighborObjectPath(ip, mac);
+        staticNeighbors.emplace(ip,
+                                std::make_shared<phosphor::network::Neighbor>(
+                                    bus, objectPath.c_str(), *this, ip, mac,
+                                    Neighbor::State::Permanent));
+    }
+}
+
 ObjectPath EthernetInterface::iP(IP::Protocol protType, std::string ipaddress,
                                  uint8_t prefixLength, std::string gateway)
 {
@@ -155,6 +178,34 @@
     return objectPath;
 }
 
+ObjectPath EthernetInterface::neighbor(std::string iPAddress,
+                                       std::string mACAddress)
+{
+    if (!isValidIP(AF_INET, iPAddress) && !isValidIP(AF_INET6, iPAddress))
+    {
+        log<level::ERR>("Not a valid IP address",
+                        entry("ADDRESS=%s", iPAddress.c_str()));
+        elog<InvalidArgument>(Argument::ARGUMENT_NAME("iPAddress"),
+                              Argument::ARGUMENT_VALUE(iPAddress.c_str()));
+    }
+    if (!mac_address::validate(mACAddress))
+    {
+        log<level::ERR>("Not a valid MAC address",
+                        entry("MACADDRESS=%s", iPAddress.c_str()));
+        elog<InvalidArgument>(Argument::ARGUMENT_NAME("mACAddress"),
+                              Argument::ARGUMENT_VALUE(mACAddress.c_str()));
+    }
+
+    std::string objectPath =
+        generateStaticNeighborObjectPath(iPAddress, mACAddress);
+    staticNeighbors.emplace(iPAddress,
+                            std::make_shared<phosphor::network::Neighbor>(
+                                bus, objectPath.c_str(), *this, iPAddress,
+                                mACAddress, Neighbor::State::Permanent));
+    manager.writeToConfigurationFile();
+    return objectPath;
+}
+
 /*
 Note: We don't have support for  ethtool now
 will enable this code once we bring the ethtool
@@ -251,6 +302,17 @@
     return hexId.str();
 }
 
+std::string EthernetInterface::generateNeighborId(const std::string& iPAddress,
+                                                  const std::string& mACAddress)
+{
+    std::stringstream hexId;
+    std::string hashString = iPAddress + mACAddress;
+
+    // Only want 8 hex digits.
+    hexId << std::hex << ((std::hash<std::string>{}(hashString)) & 0xFFFFFFFF);
+    return hexId.str();
+}
+
 void EthernetInterface::deleteObject(const std::string& ipaddress)
 {
     auto it = addrs.find(ipaddress);
@@ -263,6 +325,19 @@
     manager.writeToConfigurationFile();
 }
 
+void EthernetInterface::deleteStaticNeighborObject(const std::string& iPAddress)
+{
+    auto it = staticNeighbors.find(iPAddress);
+    if (it == staticNeighbors.end())
+    {
+        log<level::ERR>(
+            "DeleteStaticNeighborObject:Unable to find the object.");
+        return;
+    }
+    staticNeighbors.erase(it);
+    manager.writeToConfigurationFile();
+}
+
 void EthernetInterface::deleteVLANFromSystem(const std::string& interface)
 {
     auto confDir = manager.getConfDir();
@@ -330,6 +405,16 @@
     return objectPath.string();
 }
 
+std::string EthernetInterface::generateStaticNeighborObjectPath(
+    const std::string& iPAddress, const std::string& mACAddress) const
+{
+    std::experimental::filesystem::path objectPath;
+    objectPath /= objPath;
+    objectPath /= "static_neighbor";
+    objectPath /= generateNeighborId(iPAddress, mACAddress);
+    return objectPath.string();
+}
+
 bool EthernetInterface::dHCPEnabled(bool value)
 {
     if (value == EthernetInterfaceIntf::dHCPEnabled())
@@ -419,6 +504,7 @@
     // Fetch the ip address from the system
     // and create the dbus object.
     vlanIntf->createIPAddressObjects();
+    vlanIntf->createStaticNeighborObjects();
 
     this->vlanInterfaces.emplace(std::move(vlanInterfaceName),
                                  std::move(vlanIntf));
@@ -608,6 +694,15 @@
         }
     }
 
+    // Write the neighbor sections
+    for (const auto& neighbor : staticNeighbors)
+    {
+        stream << "[Neighbor]"
+               << "\n";
+        stream << "Address=" << neighbor.second->iPAddress() << "\n";
+        stream << "MACAddress=" << neighbor.second->mACAddress() << "\n";
+    }
+
     // Write the dhcp section irrespective of whether DHCP is enabled or not
     writeDHCPSection(stream);
 
diff --git a/ethernet_interface.hpp b/ethernet_interface.hpp
index c65726a..5551950 100644
--- a/ethernet_interface.hpp
+++ b/ethernet_interface.hpp
@@ -3,6 +3,7 @@
 #include "types.hpp"
 #include "util.hpp"
 #include "xyz/openbmc_project/Network/IP/Create/server.hpp"
+#include "xyz/openbmc_project/Network/Neighbor/CreateStatic/server.hpp"
 
 #include <experimental/filesystem>
 #include <sdbusplus/bus.hpp>
@@ -21,6 +22,7 @@
     sdbusplus::xyz::openbmc_project::Network::server::EthernetInterface,
     sdbusplus::xyz::openbmc_project::Network::server::MACAddress,
     sdbusplus::xyz::openbmc_project::Network::IP::server::Create,
+    sdbusplus::xyz::openbmc_project::Network::Neighbor::server::CreateStatic,
     sdbusplus::xyz::openbmc_project::Collection::server::DeleteAll>;
 
 using IP = sdbusplus::xyz::openbmc_project::Network::server::IP;
@@ -43,6 +45,8 @@
 
 class IPAddress;
 
+class Neighbor;
+
 using LinkSpeed = uint16_t;
 using DuplexMode = uint8_t;
 using Autoneg = uint8_t;
@@ -50,6 +54,7 @@
 using InterfaceName = std::string;
 using InterfaceInfo = std::tuple<LinkSpeed, DuplexMode, Autoneg>;
 using AddressMap = std::map<std::string, std::shared_ptr<IPAddress>>;
+using NeighborMap = std::map<std::string, std::shared_ptr<Neighbor>>;
 using VlanInterfaceMap =
     std::map<InterfaceName, std::unique_ptr<VlanInterface>>;
 
@@ -90,11 +95,22 @@
     ObjectPath iP(IP::Protocol addressType, std::string ipaddress,
                   uint8_t prefixLength, std::string gateway) override;
 
+    /** @brief Function to create static neighbor dbus object.
+     *  @param[in] ipAddress - IP address.
+     *  @param[in] macAddress - Low level MAC address.
+     */
+    ObjectPath neighbor(std::string iPAddress, std::string mACAddress) override;
+
     /* @brief delete the dbus object of the given ipaddress.
      * @param[in] ipaddress - IP address.
      */
     void deleteObject(const std::string& ipaddress);
 
+    /* @brief delete the dbus object of the given ipaddress.
+     * @param[in] ipaddress - IP address.
+     */
+    void deleteStaticNeighborObject(const std::string& ipAddress);
+
     /* @brief delete the vlan dbus object of the given interface.
      *        Also deletes the device file and the network file.
      * @param[in] interface - VLAN Interface.
@@ -107,6 +123,10 @@
      */
     void createIPAddressObjects();
 
+    /* @brief creates the dbus object(Neighbor) given in the neighbor list.
+     */
+    void createStaticNeighborObjects();
+
     /* @brief Gets all the ip addresses.
      * @returns the list of ipaddress.
      */
@@ -115,6 +135,14 @@
         return addrs;
     }
 
+    /* @brief Gets all the static neighbor entries.
+     * @returns Static neighbor map.
+     */
+    const NeighborMap& getStaticNeighbors() const
+    {
+        return staticNeighbors;
+    }
+
     /** Set value of DHCPEnabled */
     bool dHCPEnabled(bool value) override;
 
@@ -192,6 +220,10 @@
                                    uint8_t prefixLength,
                                    const std::string& gateway) const;
 
+    std::string
+        generateStaticNeighborObjectPath(const std::string& iPAddress,
+                                         const std::string& mACAddress) const;
+
     /** @brief generates the id by doing hash of ipaddress,
      *         prefixlength and the gateway.
      *  @param[in] ipaddress - IP address.
@@ -204,6 +236,15 @@
                                   uint8_t prefixLength,
                                   const std::string& gateway);
 
+    /** @brief generates the id by doing hash of ipaddress
+     *         and the mac address.
+     *  @param[in] iPAddress  - IP address.
+     *  @param[in] mACAddress - Gateway address.
+     *  @return hash string.
+     */
+    static std::string generateNeighborId(const std::string& iPAddress,
+                                          const std::string& mACAddress);
+
     /** @brief write the dhcp section **/
     void writeDHCPSection(std::fstream& stream);
 
@@ -232,6 +273,9 @@
     /** @brief Persistent map of IPAddress dbus objects and their names */
     AddressMap addrs;
 
+    /** @brief Persistent map of Neighbor dbus objects and their names */
+    NeighborMap staticNeighbors;
+
     /** @brief Persistent map of VLAN interface dbus objects and their names */
     VlanInterfaceMap vlanInterfaces;
 
diff --git a/neighbor.cpp b/neighbor.cpp
new file mode 100644
index 0000000..f7e6f05
--- /dev/null
+++ b/neighbor.cpp
@@ -0,0 +1,240 @@
+#include "config.h"
+
+#include "neighbor.hpp"
+
+#include "ethernet_interface.hpp"
+#include "util.hpp"
+
+#include <linux/neighbour.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <net/if.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <cstring>
+#include <stdexcept>
+#include <string_view>
+#include <system_error>
+
+namespace phosphor
+{
+namespace network
+{
+
+NeighborInfo parseNeighbor(std::string_view msg)
+{
+    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)
+    {
+        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;
+    bool set_addr = false;
+    while (!attrs.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));
+        if (hdr.rta_type == NDA_LLADDR)
+        {
+            info.mac = mac_address::fromBuf(data);
+        }
+        else if (hdr.rta_type == NDA_DST)
+        {
+            info.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;
+}
+
+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;
+}
+
+std::vector<NeighborInfo> getCurrentNeighbors()
+{
+    Descriptor netlink(getNetlink(NETLINK_ROUTE));
+    requestNeighbors(netlink());
+    return receiveNeighbors(netlink());
+}
+
+Neighbor::Neighbor(sdbusplus::bus::bus& bus, const char* objPath,
+                   EthernetInterface& parent, const std::string& ipAddress,
+                   const std::string& macAddress, State state) :
+    NeighborObj(bus, objPath, true),
+    parent(parent)
+{
+    this->iPAddress(ipAddress);
+    this->mACAddress(macAddress);
+    this->state(state);
+
+    // Emit deferred signal.
+    emit_object_added();
+}
+
+void Neighbor::delete_()
+{
+    parent.deleteStaticNeighborObject(iPAddress());
+}
+
+} // namespace network
+} // namespace phosphor
diff --git a/neighbor.hpp b/neighbor.hpp
new file mode 100644
index 0000000..e103d95
--- /dev/null
+++ b/neighbor.hpp
@@ -0,0 +1,82 @@
+#pragma once
+
+#include "types.hpp"
+#include "util.hpp"
+
+#include <netinet/in.h>
+
+#include <optional>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/server/object.hpp>
+#include <string>
+#include <vector>
+#include <xyz/openbmc_project/Network/Neighbor/server.hpp>
+#include <xyz/openbmc_project/Object/Delete/server.hpp>
+
+namespace phosphor
+{
+namespace network
+{
+
+using NeighborIntf = sdbusplus::xyz::openbmc_project::Network::server::Neighbor;
+
+using NeighborObj = sdbusplus::server::object::object<
+    NeighborIntf, sdbusplus::xyz::openbmc_project::Object::server::Delete>;
+
+class EthernetInterface;
+
+/** @class NeighborInfo
+ *  @brief Information about a neighbor from the kernel
+ */
+struct NeighborInfo
+{
+    std::string interface;
+    InAddrAny address;
+    std::optional<MacAddr> mac;
+    bool permanent;
+};
+
+/** @brief Returns a list of the current system neighbor table
+ */
+std::vector<NeighborInfo> getCurrentNeighbors();
+
+/** @class Neighbor
+ *  @brief OpenBMC network neighbor implementation.
+ *  @details A concrete implementation for the
+ *  xyz.openbmc_project.Network.Neighbor dbus interface.
+ */
+class Neighbor : public NeighborObj
+{
+  public:
+    using State = NeighborIntf::State;
+
+    Neighbor() = delete;
+    Neighbor(const Neighbor&) = delete;
+    Neighbor& operator=(const Neighbor&) = delete;
+    Neighbor(Neighbor&&) = delete;
+    Neighbor& operator=(Neighbor&&) = delete;
+    virtual ~Neighbor() = default;
+
+    /** @brief Constructor to put object onto bus at a dbus path.
+     *  @param[in] bus - Bus to attach to.
+     *  @param[in] objPath - Path to attach at.
+     *  @param[in] parent - Parent object.
+     *  @param[in] ipAddress - IP address.
+     *  @param[in] macAddress - Low level MAC address.
+     *  @param[in] state - The state of the neighbor entry.
+     */
+    Neighbor(sdbusplus::bus::bus& bus, const char* objPath,
+             EthernetInterface& parent, const std::string& ipAddress,
+             const std::string& macAddress, State state);
+
+    /** @brief Delete this d-bus object.
+     */
+    void delete_() override;
+
+  private:
+    /** @brief Parent Object. */
+    EthernetInterface& parent;
+};
+
+} // namespace network
+} // namespace phosphor
diff --git a/network_manager.cpp b/network_manager.cpp
index fa5da0f..043d7a2 100644
--- a/network_manager.cpp
+++ b/network_manager.cpp
@@ -150,6 +150,7 @@
             bus, objPath.string(), dhcp, *this);
 
         intf->createIPAddressObjects();
+        intf->createStaticNeighborObjects();
 
         this->interfaces.emplace(
             std::make_pair(std::move(interface), std::move(intf)));
diff --git a/rtnetlink_server.cpp b/rtnetlink_server.cpp
index 732055a..1c4fd53 100644
--- a/rtnetlink_server.cpp
+++ b/rtnetlink_server.cpp
@@ -14,6 +14,7 @@
 #include <memory>
 #include <phosphor-logging/elog-errors.hpp>
 #include <phosphor-logging/log.hpp>
+#include <string_view>
 #include <xyz/openbmc_project/Common/error.hpp>
 
 namespace phosphor
@@ -26,7 +27,7 @@
 namespace rtnetlink
 {
 
-static bool shouldRefresh(const struct nlmsghdr& hdr)
+static bool shouldRefresh(const struct nlmsghdr& hdr, std::string_view data)
 {
     switch (hdr.nlmsg_type)
     {
@@ -37,6 +38,18 @@
         {
             return true;
         }
+        case RTM_NEWNEIGH:
+        case RTM_DELNEIGH:
+        {
+            struct ndmsg ndm;
+            if (data.size() < sizeof(ndm))
+            {
+                return false;
+            }
+            memcpy(&ndm, data.data(), sizeof(ndm));
+            // We only want to refresh for static neighbors
+            return ndm.ndm_state & NUD_PERMANENT;
+        }
     }
 
     return false;
@@ -57,7 +70,10 @@
                (netLinkHeader->nlmsg_type != NLMSG_DONE);
              netLinkHeader = NLMSG_NEXT(netLinkHeader, len))
         {
-            if (shouldRefresh(*netLinkHeader))
+            std::string_view data(
+                reinterpret_cast<const char*>(NLMSG_DATA(netLinkHeader)),
+                netLinkHeader->nlmsg_len - NLMSG_HDRLEN);
+            if (shouldRefresh(*netLinkHeader, data))
             {
                 // starting the timer here to make sure that we don't want
                 // create the child objects multiple times.
@@ -125,7 +141,7 @@
     std::memset(&addr, 0, sizeof(addr));
     addr.nl_family = AF_NETLINK;
     addr.nl_groups = RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR |
-                     RTMGRP_IPV4_ROUTE | RTMGRP_IPV6_ROUTE;
+                     RTMGRP_IPV4_ROUTE | RTMGRP_IPV6_ROUTE | RTMGRP_NEIGH;
 
     if (bind(smartSock(), (struct sockaddr*)&addr, sizeof(addr)) < 0)
     {
diff --git a/test/Makefile.am b/test/Makefile.am
index 78636fc..ffa4655 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -49,6 +49,7 @@
 			$(top_builddir)/network_manager.o \
 			$(top_builddir)/network_config.o \
 			$(top_builddir)/ipaddress.o \
+			$(top_builddir)/neighbor.o \
 			$(top_builddir)/routing_table.o \
 			$(top_builddir)/util.o \
 			$(top_builddir)/rtnetlink_server.o \
@@ -57,7 +58,8 @@
 			$(top_builddir)/config_parser.o \
 			$(top_builddir)/vlan_interface.o \
 			$(top_builddir)/xyz/openbmc_project/Network/VLAN/Create/phosphor_network_manager-server.o \
-			$(top_builddir)/xyz/openbmc_project/Network/IP/Create/phosphor_network_manager-server.o
+			$(top_builddir)/xyz/openbmc_project/Network/IP/Create/phosphor_network_manager-server.o \
+			$(top_builddir)/xyz/openbmc_project/Network/Neighbor/CreateStatic/phosphor_network_manager-server.o
 
 test_dns_updater_LDADD = $(top_builddir)/dns_updater.o
 test_watch_LDADD = $(top_builddir)/watch.o
diff --git a/xyz/openbmc_project/Network/Neighbor/CreateStatic.interface.yaml b/xyz/openbmc_project/Network/Neighbor/CreateStatic.interface.yaml
new file mode 100644
index 0000000..a4ceabb
--- /dev/null
+++ b/xyz/openbmc_project/Network/Neighbor/CreateStatic.interface.yaml
@@ -0,0 +1,21 @@
+description: >
+methods:
+    - name: Neighbor
+      description: >
+          Create a static neighbor entry.
+      parameters:
+        - name: IPAddress
+          type: string
+          description: >
+              IP Address.
+        - name: MACAddress
+          type: string
+          description: >
+              MAC Address.
+      returns:
+        - name: Path
+          type: path
+          description: >
+            The path for the created neighbor object.
+      errors:
+        - xyz.openbmc_project.Common.Error.InvalidArgument