spawn one rmcpp bridge per interface

According to the new architecture, each bridge should be a separate
process, or at the very least, be attached on a separate D-Bus
connection with a well-known name that corresponds to the channel name.

This commit brings netipmid to a state where it can be launched as:
systemctl start phosphor-ipmi-net@eth0

with a parameterized unit file and socket file that binds the socket to
the interface specified. If not launched this way, it will by default be
bound to all interfaces.

This includes the new parameterized service and socket file and the
autoconf/automake magic to install them to the correct place.

Tested-by: launch netipmid via current unit file/socket file
           launch netipmid via parameterize unit file/socket file

Change-Id: Ib202015fb560da269e535f11c0ac316b17f6c262
Signed-off-by: Vernon Mauery <vernon.mauery@linux.intel.com>
diff --git a/Makefile.am b/Makefile.am
index 7d161b3..10dac91 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -81,5 +81,11 @@
 	$(PHOSPHOR_DBUS_INTERFACES_CFLAGS) \
 	$(LIBIPMID_CFLAGS)
 
+if HAVE_SYSTEMD
+SYSTEMD_UNIT = phosphor-ipmi-net@.service
+SYSTEMD_SOCKET = phosphor-ipmi-net@.socket
+systemdsystemunit_DATA = $(SYSTEMD_UNIT) $(SYSTEMD_SOCKET)
+endif  # HAVE_SYSTEMD
+
 SUBDIRS = test
 
diff --git a/command_table.cpp b/command_table.cpp
index 04e1ac2..f4e3ece 100644
--- a/command_table.cpp
+++ b/command_table.cpp
@@ -64,7 +64,8 @@
             std::get<session::Manager&>(singletonPool)
                 .getSession(handler->sessionID);
         std::map<std::string, ipmi::Value> options = {
-            {"userId", ipmi::Value(ipmi::ipmiUserGetUserId(session->userName))},
+            {"userId", ipmi::Value(static_cast<int>(
+                           ipmi::ipmiUserGetUserId(session->userName)))},
             {"privilege", ipmi::Value(static_cast<int>(session->curPrivLevel))},
         };
         bus->async_method_call(
diff --git a/configure.ac b/configure.ac
index 305b7f3..36b0df4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -49,6 +49,23 @@
 PKG_CHECK_MODULES([LIBIPMID], [libipmid])
 AC_CHECK_LIB([mapper], [mapper_get_service], ,[AC_MSG_ERROR([Could not find libmapper...openbmc/phosphor-objmgr package required])])
 
+# systemd unit general configuration
+PKG_PROG_PKG_CONFIG
+AC_ARG_WITH([systemdsystemunitdir],
+            [AS_HELP_STRING([--with-systemdsystemunitdir=DIR],
+                            [Directory for systemd service files])], [],
+            [with_systemdsystemunitdir=auto])
+AS_IF([test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"],
+      [def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd)
+       AS_IF([test "x$def_systemdsystemunitdir" = "x"],
+             [AS_IF([test "x$with_systemdsystemunitdir" = "xyes"],
+                    [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])],
+                    [with_systemdsystemunitdir=no])],
+             [with_systemdsystemunitdir="$def_systemdsystemunitdir"])])
+AS_IF([test "x$with_systemdsystemunitdir" != "xno"],
+      [AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])])
+AM_CONDITIONAL([HAVE_SYSTEMD], [test "x$with_systemdsystemunitdir" != "xno"])
+
 # Checks for header files.
 AC_CHECK_HEADER(systemd/sd-bus.h, ,[AC_MSG_ERROR([Could not find systemd/sd-bus.h...systemd development package required])])
 
diff --git a/main.cpp b/main.cpp
index 8fc27f4..d26e032 100644
--- a/main.cpp
+++ b/main.cpp
@@ -16,6 +16,7 @@
 #include <systemd/sd-event.h>
 #include <unistd.h>
 
+#include <CLI/CLI.hpp>
 #include <phosphor-logging/log.hpp>
 #include <sdbusplus/asio/connection.hpp>
 #include <tuple>
@@ -58,8 +59,17 @@
     return interfaceLAN1;
 }
 
-int main()
+int main(int argc, char* argv[])
 {
+    CLI::App app("KCS RMCP+ bridge");
+    std::string channel;
+    app.add_option("-c,--channel", channel, "channel name. e.g., eth0");
+    uint16_t port = ipmi::rmcpp::defaultPort;
+    app.add_option("-p,--port", port, "port number");
+    bool verbose = false;
+    app.add_option("-v,--verbose", verbose, "print more verbose output");
+    CLI11_PARSE(app, argc, argv);
+
     // Connect to system bus
     auto rc = sd_bus_default_system(&bus);
     if (rc < 0)
@@ -81,6 +91,12 @@
     // Register the phosphor-net-ipmid SOL commands
     sol::command::registerCommands();
 
+    auto& loop = std::get<eventloop::EventLoop&>(singletonPool);
+    if (loop.setupSocket(sdbusp, channel))
+    {
+        return EXIT_FAILURE;
+    }
+
     // Start Event Loop
-    return std::get<eventloop::EventLoop&>(singletonPool).startEventLoop();
+    return loop.startEventLoop();
 }
diff --git a/phosphor-ipmi-net@.service b/phosphor-ipmi-net@.service
new file mode 100644
index 0000000..77a800f
--- /dev/null
+++ b/phosphor-ipmi-net@.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=Network IPMI daemon
+Requires=phosphor-ipmi-host.service
+After=phosphor-ipmi-host.service
+
+[Service]
+ExecStart=/usr/bin/netipmid -c %i
+SyslogIdentifier=netipmid-%i
+Restart=always
+RuntimeDirectory = ipmi
+RuntimeDirectoryPreserve = yes
+StateDirectory = ipmi
+
+[Install]
+DefaultInstance=eth0
+WantedBy=multi-user.target
+RequiredBy=
diff --git a/phosphor-ipmi-net@.socket b/phosphor-ipmi-net@.socket
new file mode 100644
index 0000000..5e56541
--- /dev/null
+++ b/phosphor-ipmi-net@.socket
@@ -0,0 +1,7 @@
+[Socket]
+ListenDatagram=623
+BindToDevice=sys-subsystem-net-devices-%i.device
+
+[Install]
+WantedBy=sockets.target
+
diff --git a/sd_event_loop.cpp b/sd_event_loop.cpp
index ee25960..7a647cc 100644
--- a/sd_event_loop.cpp
+++ b/sd_event_loop.cpp
@@ -46,6 +46,89 @@
                           });
 }
 
+int EventLoop::setupSocket(std::shared_ptr<sdbusplus::asio::connection>& bus,
+                           std::string iface, uint16_t reqPort)
+{
+    static constexpr const char* unboundIface = "rmcpp";
+    if (iface == "")
+    {
+        iface = unboundIface;
+    }
+    // Create our own socket if SysD did not supply one.
+    int listensFdCount = sd_listen_fds(0);
+    if (listensFdCount > 1)
+    {
+        log<level::ERR>("Too many file descriptors received");
+        return EXIT_FAILURE;
+    }
+    if (listensFdCount == 1)
+    {
+        int openFd = SD_LISTEN_FDS_START;
+        if (!sd_is_socket(openFd, AF_UNSPEC, SOCK_DGRAM, -1))
+        {
+            log<level::ERR>("Failed to set up systemd-passed socket");
+            return EXIT_FAILURE;
+        }
+        udpSocket = std::make_shared<boost::asio::ip::udp::socket>(
+            *io, boost::asio::ip::udp::v6(), openFd);
+    }
+    else
+    {
+        // asio does not natively offer a way to bind to an interface
+        // so it must be done in steps
+        boost::asio::ip::udp::endpoint ep(boost::asio::ip::udp::v6(), reqPort);
+        udpSocket = std::make_shared<boost::asio::ip::udp::socket>(*io);
+        udpSocket->open(ep.protocol());
+        // bind
+        udpSocket->set_option(
+            boost::asio::ip::udp::socket::reuse_address(true));
+        udpSocket->bind(ep);
+    }
+    // SO_BINDTODEVICE
+    char nameout[IFNAMSIZ];
+    unsigned int lenout = sizeof(nameout);
+    if ((::getsockopt(udpSocket->native_handle(), SOL_SOCKET, SO_BINDTODEVICE,
+                      nameout, &lenout) == -1))
+    {
+        log<level::ERR>("Failed to read bound device",
+                        entry("ERROR=%s", strerror(errno)));
+    }
+    if (iface != nameout && iface != unboundIface)
+    {
+        // SO_BINDTODEVICE
+        if ((::setsockopt(udpSocket->native_handle(), SOL_SOCKET,
+                          SO_BINDTODEVICE, iface.c_str(),
+                          iface.size() + 1) == -1))
+        {
+            log<level::ERR>("Failed to bind to requested interface",
+                            entry("ERROR=%s", strerror(errno)));
+            return EXIT_FAILURE;
+        }
+    }
+    // cannot be constexpr because it gets passed by address
+    const int option_enabled = 1;
+    // common socket stuff; set options to get packet info (DST addr)
+    ::setsockopt(udpSocket->native_handle(), IPPROTO_IP, IP_PKTINFO,
+                 &option_enabled, sizeof(option_enabled));
+    ::setsockopt(udpSocket->native_handle(), IPPROTO_IPV6, IPV6_RECVPKTINFO,
+                 &option_enabled, sizeof(option_enabled));
+
+    // set the dbus name
+    std::string busName = "xyz.openbmc_project.Ipmi.Channel." + iface;
+    try
+    {
+        bus->request_name(busName.c_str());
+    }
+    catch (const std::exception& e)
+    {
+        log<level::ERR>("Failed to acquire D-Bus name",
+                        entry("NAME=%s", busName.c_str()),
+                        entry("ERROR=%s", e.what()));
+        return EXIT_FAILURE;
+    }
+    return 0;
+}
+
 int EventLoop::startEventLoop()
 {
     // set up boost::asio signal handling
@@ -57,32 +140,6 @@
             io->stop();
         });
 
-    // Create our own socket if SysD did not supply one.
-    int listensFdCount = sd_listen_fds(0);
-    if (listensFdCount == 1)
-    {
-        if (sd_is_socket(SD_LISTEN_FDS_START, AF_UNSPEC, SOCK_DGRAM, -1))
-        {
-            udpSocket = std::make_shared<boost::asio::ip::udp::socket>(
-                *io, boost::asio::ip::udp::v6(), SD_LISTEN_FDS_START);
-        }
-    }
-    else if (listensFdCount > 1)
-    {
-        log<level::ERR>("Too many file descriptors received");
-        return EXIT_FAILURE;
-    }
-    if (!udpSocket)
-    {
-        udpSocket = std::make_shared<boost::asio::ip::udp::socket>(
-            *io, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v6(),
-                                                IPMI_STD_PORT));
-        if (!udpSocket)
-        {
-            log<level::ERR>("Failed to start listening on RMCP socket");
-            return EXIT_FAILURE;
-        }
-    }
     startRmcpReceive();
 
     io->run();
diff --git a/sd_event_loop.hpp b/sd_event_loop.hpp
index a626490..54b2946 100644
--- a/sd_event_loop.hpp
+++ b/sd_event_loop.hpp
@@ -7,6 +7,15 @@
 #include <boost/asio/io_context.hpp>
 #include <chrono>
 #include <map>
+#include <sdbusplus/asio/connection.hpp>
+
+namespace ipmi
+{
+namespace rmcpp
+{
+constexpr uint16_t defaultPort = 623;
+} // namespace rmcpp
+} // namespace ipmi
 
 namespace eventloop
 {
@@ -31,6 +40,13 @@
      */
     int startEventLoop();
 
+    /** @brief Set up the socket (if systemd has not already) and
+     *         make sure that the bus name matches the specified channel
+     */
+    int setupSocket(std::shared_ptr<sdbusplus::asio::connection>& bus,
+                    std::string iface,
+                    uint16_t reqPort = ipmi::rmcpp::defaultPort);
+
   private:
     /** @brief async handler for incoming udp packets */
     void handleRmcpPacket();
diff --git a/socket_channel.hpp b/socket_channel.hpp
index ebe0c8f..5742944 100644
--- a/socket_channel.hpp
+++ b/socket_channel.hpp
@@ -1,9 +1,15 @@
 #pragma once
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/types.h>
 
 #include <boost/asio/ip/udp.hpp>
 #include <memory>
+#include <optional>
+#include <phosphor-logging/log.hpp>
 #include <string>
 #include <tuple>
+#include <variant>
 #include <vector>
 
 namespace udpsocket
@@ -48,7 +54,27 @@
      */
     std::string getRemoteAddress() const
     {
-        return endpoint.address().to_string();
+        const char* retval = nullptr;
+        if (sockAddrSize == sizeof(sockaddr_in))
+        {
+            char ipv4addr[INET_ADDRSTRLEN];
+            retval =
+                inet_ntop(AF_INET, &remoteSockAddr, ipv4addr, sizeof(ipv4addr));
+        }
+        else if (sockAddrSize == sizeof(sockaddr_in6))
+        {
+            char ipv6addr[INET6_ADDRSTRLEN];
+            retval = inet_ntop(AF_INET6, &remoteSockAddr, ipv6addr,
+                               sizeof(ipv6addr));
+        }
+        if (retval)
+        {
+            return retval;
+        }
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Error in inet_ntop",
+            phosphor::logging::entry("ERROR=%s", strerror(errno)));
+        return std::string();
     }
 
     /**
@@ -59,9 +85,19 @@
      * @return Port number
      *
      */
-    auto getPort() const
+    uint16_t getPort() const
     {
-        return endpoint.port();
+        if (sockAddrSize == sizeof(sockaddr_in))
+        {
+            return ntohs(reinterpret_cast<const sockaddr_in*>(&remoteSockAddr)
+                             ->sin_port);
+        }
+        if (sockAddrSize == sizeof(sockaddr_in6))
+        {
+            return ntohs(reinterpret_cast<const sockaddr_in6*>(&remoteSockAddr)
+                             ->sin6_port);
+        }
+        return 0;
     }
 
     /**
@@ -77,14 +113,43 @@
      */
     std::tuple<int, std::vector<uint8_t>> read()
     {
+        // cannot use the standard asio reading mechanism because it does not
+        // provide a mechanism to reach down into the depths and use a msghdr
         std::vector<uint8_t> packet(socket->available());
-        try
+        iovec iov = {packet.data(), packet.size()};
+        char msgCtrl[1024];
+        msghdr msg = {&remoteSockAddr, sizeof(remoteSockAddr), &iov, 1,
+                      msgCtrl,         sizeof(msgCtrl),        0};
+
+        ssize_t bytesReceived = recvmsg(socket->native_handle(), &msg, 0);
+        // Read of the packet failed
+        if (bytesReceived < 0)
         {
-            socket->receive_from(boost::asio::buffer(packet), endpoint);
+            // something bad happened; bail
+            phosphor::logging::log<phosphor::logging::level::ERR>(
+                "Error in recvmsg",
+                phosphor::logging::entry("ERROR=%s", strerror(errno)));
+            return std::make_tuple(-errno, std::vector<uint8_t>());
         }
-        catch (const boost::system::system_error& e)
+        // save the size of either ipv4 or i4v6 sockaddr
+        sockAddrSize = msg.msg_namelen;
+
+        // extract the destination address from the message
+        cmsghdr* cmsg;
+        for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != 0;
+             cmsg = CMSG_NXTHDR(&msg, cmsg))
         {
-            return std::make_tuple(e.code().value(), std::vector<uint8_t>());
+            if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO)
+            {
+                // save local address from the pktinfo4
+                pktinfo4 = *reinterpret_cast<in_pktinfo*>(CMSG_DATA(cmsg));
+            }
+            if (cmsg->cmsg_level == IPPROTO_IPV6 &&
+                cmsg->cmsg_type == IPV6_PKTINFO)
+            {
+                // save local address from the pktinfo6
+                pktinfo6 = *reinterpret_cast<in6_pktinfo*>(CMSG_DATA(cmsg));
+            }
         }
         return std::make_tuple(0, packet);
     }
@@ -97,20 +162,45 @@
      *  @param [in] inBuffer
      *      The vector would be the buffer of data to write to the socket.
      *
-     *  @return In case of success the return code is 0 and return code is
-     *          < 0 in case of failure.
+     *  @return In case of success the return code is the number of bytes
+     *          written and return code is < 0 in case of failure.
      */
     int write(const std::vector<uint8_t>& inBuffer)
     {
-        try
+        // in order to make sure packets go back out from the same
+        // IP address they came in on, sendmsg must be used instead
+        // of the boost::asio::ip::send or sendto
+        iovec iov = {const_cast<uint8_t*>(inBuffer.data()), inBuffer.size()};
+        char msgCtrl[1024];
+        msghdr msg = {&remoteSockAddr, sockAddrSize,    &iov, 1,
+                      msgCtrl,         sizeof(msgCtrl), 0};
+        int cmsg_space = 0;
+        cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
+        if (pktinfo6)
         {
-            socket->send_to(boost::asio::buffer(inBuffer), endpoint);
+            cmsg->cmsg_level = IPPROTO_IPV6;
+            cmsg->cmsg_type = IPV6_PKTINFO;
+            cmsg->cmsg_len = CMSG_LEN(sizeof(in6_pktinfo));
+            *reinterpret_cast<in6_pktinfo*>(CMSG_DATA(cmsg)) = *pktinfo6;
+            cmsg_space += CMSG_SPACE(sizeof(in6_pktinfo));
         }
-        catch (const boost::system::system_error& e)
+        else if (pktinfo4)
         {
-            return e.code().value();
+            cmsg->cmsg_level = IPPROTO_IP;
+            cmsg->cmsg_type = IP_PKTINFO;
+            cmsg->cmsg_len = CMSG_LEN(sizeof(in_pktinfo));
+            *reinterpret_cast<in_pktinfo*>(CMSG_DATA(cmsg)) = *pktinfo4;
+            cmsg_space += CMSG_SPACE(sizeof(in_pktinfo));
         }
-        return 0;
+        msg.msg_controllen = cmsg_space;
+        int ret = sendmsg(socket->native_handle(), &msg, 0);
+        if (ret < 0)
+        {
+            phosphor::logging::log<phosphor::logging::level::ERR>(
+                "Error in sendmsg",
+                phosphor::logging::entry("ERROR=%s", strerror(errno)));
+        }
+        return ret;
     }
 
     /**
@@ -123,7 +213,10 @@
 
   private:
     std::shared_ptr<boost::asio::ip::udp::socket> socket;
-    boost::asio::ip::udp::endpoint endpoint{};
+    sockaddr_storage remoteSockAddr;
+    socklen_t sockAddrSize;
+    std::optional<in_pktinfo> pktinfo4;
+    std::optional<in6_pktinfo> pktinfo6;
 };
 
 } // namespace udpsocket