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