ncsi: Add a NCSI-over-MCTP transport

Add a facility for performing NCSI commands over a NCSI-over-MCTP
interface, alongside the existing Netlink transport.

This adds a new Interface subclass, MCTPInterface, which performs the
MCTP encapsulation/decapsulation, over an AF_MCTP socket.

Tested: able to perform NCSI commands over a MCTP link, to both emulated
and hardware NIC devices. The -m argument can now target a NIC using
MCTP.

For example, sending a raw command to perform a Get Version ID (type
0x15):

    root@bmc:~# ncsi-cmd -m 9 --package 0 raw 0x15
    <7> Command: type 0x15, payload 0 bytes:
    <7> Response 60 bytes: 00 01 00 2b 95 [...]

Change-Id: I9a7bfddfc4fd1b5bb8d0bff187936a0258d3dade
Signed-off-by: Jeremy Kerr <jk@codeconstruct.com.au>
diff --git a/src/ncsi_cmd.cpp b/src/ncsi_cmd.cpp
index 07a8d7e..83acb85 100644
--- a/src/ncsi_cmd.cpp
+++ b/src/ncsi_cmd.cpp
@@ -18,6 +18,7 @@
 
 #include <assert.h>
 #include <getopt.h>
+#include <linux/mctp.h>
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
@@ -43,10 +44,21 @@
     std::optional<unsigned int> channel;
 };
 
+struct MCTPAddress
+{
+    int network;
+    uint8_t eid;
+};
+
+/* MCTP EIDs below 8 are invalid, 255 is broadcast */
+static constexpr uint8_t MCTP_EID_MIN = 8;
+static constexpr uint8_t MCTP_EID_MAX = 254;
+
 const struct option options[] = {
     {"package", required_argument, NULL, 'p'},
     {"channel", required_argument, NULL, 'c'},
     {"interface", required_argument, NULL, 'i'},
+    {"mctp", required_argument, NULL, 'm'},
     {"help", no_argument, NULL, 'h'},
     {0, 0, 0, 0},
 };
@@ -61,10 +73,12 @@
         "\n"
         "Global options:\n"
         "    --interface IFACE, -i  Specify net device by ifindex.\n"
+        "    --mctp [NET,]EID, -m   Specify MCTP network device.\n"
         "    --package PACKAGE, -p  Specify a package.\n"
         "    --channel CHANNEL, -c  Specify a channel.\n"
         "\n"
-        "Both --interface/-i and --package/-p are required.\n"
+        "A --package/-p argument is required, as well as interface type "
+        "(--interface/-i or --mctp/-m)\n"
         "\n"
         "Subcommands:\n"
         "\n"
@@ -94,6 +108,51 @@
     return {};
 }
 
+static std::optional<MCTPAddress> parseMCTPAddress(const std::string& str)
+{
+    std::string::size_type sep = str.find(',');
+    std::string eid_str;
+    MCTPAddress addr;
+
+    if (sep == std::string::npos)
+    {
+        addr.network = MCTP_NET_ANY;
+        eid_str = str;
+    }
+    else
+    {
+        std::string net_str = str.substr(0, sep);
+        try
+        {
+            addr.network = stoi(net_str);
+        }
+        catch (const std::exception& e)
+        {
+            return {};
+        }
+        eid_str = str.substr(sep + 1);
+    }
+
+    unsigned long tmp;
+    try
+    {
+        tmp = stoul(eid_str);
+    }
+    catch (const std::exception& e)
+    {
+        return {};
+    }
+
+    if (tmp < MCTP_EID_MIN || tmp > MCTP_EID_MAX)
+    {
+        return {};
+    }
+
+    addr.eid = tmp;
+
+    return addr;
+}
+
 static std::optional<std::vector<unsigned char>>
     parsePayload(int argc, const char* const argv[])
 {
@@ -170,6 +229,7 @@
     parseGlobalOptions(int argc, char* const* argv)
 {
     std::optional<unsigned int> chan, package, interface;
+    std::optional<MCTPAddress> mctp;
     const char* progname = argv[0];
     GlobalOptions opts{};
 
@@ -178,7 +238,7 @@
         /* We're using + here as we want to stop parsing at the subcommand
          * name
          */
-        int opt = getopt_long(argc, argv, "+p:c:i:h", options, NULL);
+        int opt = getopt_long(argc, argv, "+p:c:i:m:h", options, NULL);
         if (opt == -1)
         {
             break;
@@ -202,6 +262,14 @@
                 }
                 break;
 
+            case 'm':
+                mctp = parseMCTPAddress(optarg);
+                if (!mctp.has_value())
+                {
+                    return {};
+                }
+                break;
+
             case 'c':
                 chan = parseUnsigned(optarg, "channel");
                 if (!chan.has_value())
@@ -218,9 +286,24 @@
         }
     }
 
-    if (!interface.has_value())
+    if (interface.has_value() && mctp.has_value())
     {
-        std::cerr << "Missing interface, add an --interface argument\n";
+        std::cerr << "Only one of --interface or --mctp can be provided\n";
+        return {};
+    }
+    else if (interface.has_value())
+    {
+        opts.interface = std::make_unique<NetlinkInterface>(*interface);
+    }
+    else if (mctp.has_value())
+    {
+        MCTPAddress m = *mctp;
+        opts.interface = std::make_unique<MCTPInterface>(m.network, m.eid);
+    }
+    else
+    {
+        std::cerr << "Missing interface description, "
+                     "add a --mctp or --interface argument\n";
         return {};
     }
 
@@ -230,7 +313,6 @@
         return {};
     }
 
-    opts.interface = std::make_unique<NetlinkInterface>(*interface);
     opts.package = *package;
 
     return std::make_tuple(std::move(opts), optind);
diff --git a/src/ncsi_util.cpp b/src/ncsi_util.cpp
index c9ac7b5..1f505f0 100644
--- a/src/ncsi_util.cpp
+++ b/src/ncsi_util.cpp
@@ -1,14 +1,17 @@
 #include "ncsi_util.hpp"
 
+#include <linux/mctp.h>
 #include <linux/ncsi.h>
 #include <netlink/genl/ctrl.h>
 #include <netlink/genl/genl.h>
 #include <netlink/netlink.h>
+#include <unistd.h>
 
 #include <phosphor-logging/lg2.hpp>
 
 #include <optional>
 #include <span>
+#include <system_error>
 #include <vector>
 
 namespace phosphor
@@ -563,6 +566,184 @@
     return 0;
 }
 
+static const uint8_t MCTP_TYPE_NCSI = 2;
+
+struct NCSIResponsePayload
+{
+    uint16_t response;
+    uint16_t reason;
+};
+
+std::optional<NCSIResponse> MCTPInterface::sendCommand(NCSICommand& cmd)
+{
+    static constexpr uint8_t iid = 0;  /* we only have one cmd outstanding */
+    static constexpr uint8_t mcid = 0; /* no need to distinguish controllers */
+    static constexpr size_t maxRespLen = 16384;
+    size_t payloadLen, padLen;
+    ssize_t wlen, rlen;
+
+    payloadLen = cmd.payload.size();
+
+    internal::NCSIPacketHeader cmdHeader{};
+    cmdHeader.MCID = mcid;
+    cmdHeader.revision = 1;
+    cmdHeader.id = iid;
+    cmdHeader.type = cmd.opcode;
+    cmdHeader.channel = (uint8_t)(cmd.package << 5 | cmd.getChannel());
+    cmdHeader.length = htons(payloadLen);
+
+    struct iovec iov[3];
+    iov[0].iov_base = &cmdHeader;
+    iov[0].iov_len = sizeof(cmdHeader);
+    iov[1].iov_base = cmd.payload.data();
+    iov[1].iov_len = payloadLen;
+
+    /* the checksum must appear on a 4-byte boundary */
+    padLen = 4 - (payloadLen & 0x3);
+    if (padLen == 4)
+    {
+        padLen = 0;
+    }
+    uint8_t crc32buf[8] = {};
+    /* todo: set csum; zeros currently indicate no checksum present */
+    uint32_t crc32 = 0;
+
+    memcpy(crc32buf + padLen, &crc32, sizeof(crc32));
+    padLen += sizeof(crc32);
+
+    iov[2].iov_base = crc32buf;
+    iov[2].iov_len = padLen;
+
+    struct sockaddr_mctp addr = {};
+    addr.smctp_family = AF_MCTP;
+    addr.smctp_network = net;
+    addr.smctp_addr.s_addr = eid;
+    addr.smctp_tag = MCTP_TAG_OWNER;
+    addr.smctp_type = MCTP_TYPE_NCSI;
+
+    struct msghdr msg = {};
+    msg.msg_name = &addr;
+    msg.msg_namelen = sizeof(addr);
+    msg.msg_iov = iov;
+    msg.msg_iovlen = 3;
+
+    wlen = sendmsg(sd, &msg, 0);
+    if (wlen < 0)
+    {
+        lg2::error("Failed to send MCTP message, ERRNO: {ERRNO}", "ERRNO",
+                   -wlen);
+        return {};
+    }
+    else if ((size_t)wlen != sizeof(cmdHeader) + payloadLen + padLen)
+    {
+        lg2::error("Short write sending MCTP message, LEN: {LEN}", "LEN", wlen);
+        return {};
+    }
+
+    internal::NCSIPacketHeader* respHeader;
+    NCSIResponsePayload* respPayload;
+    NCSIResponse resp{};
+
+    resp.full_payload.resize(maxRespLen);
+    iov[0].iov_len = resp.full_payload.size();
+    iov[0].iov_base = resp.full_payload.data();
+
+    msg.msg_name = &addr;
+    msg.msg_namelen = sizeof(addr);
+    msg.msg_iov = iov;
+    msg.msg_iovlen = 1;
+
+    /* we have set SO_RCVTIMEO, so this won't block forever... */
+    rlen = recvmsg(sd, &msg, MSG_TRUNC);
+    if (rlen < 0)
+    {
+        lg2::error("Failed to read MCTP response, ERRNO: {ERRNO}", "ERRNO",
+                   -rlen);
+        return {};
+    }
+    else if ((size_t)rlen < sizeof(*respHeader) + sizeof(*respPayload))
+    {
+        lg2::error("Short read receiving MCTP message, LEN: {LEN}", "LEN",
+                   rlen);
+        return {};
+    }
+    else if ((size_t)rlen > maxRespLen)
+    {
+        lg2::error("MCTP response is too large, LEN: {LEN}", "LEN", rlen);
+        return {};
+    }
+
+    resp.full_payload.resize(rlen);
+
+    respHeader =
+        reinterpret_cast<decltype(respHeader)>(resp.full_payload.data());
+
+    /* header validation */
+    if (respHeader->MCID != mcid)
+    {
+        lg2::error("Invalid MCID {MCID} in response", "MCID", lg2::hex,
+                   respHeader->MCID);
+        return {};
+    }
+
+    if (respHeader->id != iid)
+    {
+        lg2::error("Invalid IID {IID} in response", "IID", lg2::hex,
+                   respHeader->id);
+        return {};
+    }
+
+    if (respHeader->type != (cmd.opcode | 0x80))
+    {
+        lg2::error("Invalid opcode {OPCODE} in response", "OPCODE", lg2::hex,
+                   respHeader->type);
+        return {};
+    }
+
+    int rc = resp.parseFullPayload();
+    if (rc)
+    {
+        return {};
+    }
+
+    return resp;
+}
+
+std::string MCTPInterface::toString()
+{
+    return std::to_string(net) + "," + std::to_string(eid);
+}
+
+MCTPInterface::MCTPInterface(int net, uint8_t eid) : net(net), eid(eid)
+{
+    static const struct timeval receiveTimeout = {
+        .tv_sec = 1,
+        .tv_usec = 0,
+    };
+
+    int _sd = socket(AF_MCTP, SOCK_DGRAM, 0);
+    if (_sd < 0)
+    {
+        throw std::system_error(errno, std::system_category(),
+                                "Can't create MCTP socket");
+    }
+
+    int rc = setsockopt(_sd, SOL_SOCKET, SO_RCVTIMEO, &receiveTimeout,
+                        sizeof(receiveTimeout));
+    if (rc != 0)
+    {
+        throw std::system_error(errno, std::system_category(),
+                                "Can't set socket receive timemout");
+    }
+
+    sd = _sd;
+}
+
+MCTPInterface::~MCTPInterface()
+{
+    close(sd);
+}
+
 } // namespace ncsi
 } // namespace network
 } // namespace phosphor
diff --git a/src/ncsi_util.hpp b/src/ncsi_util.hpp
index 250c096..27397c8 100644
--- a/src/ncsi_util.hpp
+++ b/src/ncsi_util.hpp
@@ -153,6 +153,20 @@
     int ifindex;
 };
 
+struct MCTPInterface : Interface
+{
+    std::optional<NCSIResponse> sendCommand(NCSICommand& cmd);
+    std::string toString();
+
+    MCTPInterface(int net, uint8_t eid);
+    ~MCTPInterface();
+
+  private:
+    int sd;
+    int net;
+    uint8_t eid;
+};
+
 } // namespace ncsi
 } // namespace network
 } // namespace phosphor