aboutsummaryrefslogtreecommitdiff
path: root/usr.sbin/ctld/nvmf.cc
diff options
context:
space:
mode:
Diffstat (limited to 'usr.sbin/ctld/nvmf.cc')
-rw-r--r--usr.sbin/ctld/nvmf.cc478
1 files changed, 478 insertions, 0 deletions
diff --git a/usr.sbin/ctld/nvmf.cc b/usr.sbin/ctld/nvmf.cc
new file mode 100644
index 000000000000..d1240bfa4f6c
--- /dev/null
+++ b/usr.sbin/ctld/nvmf.cc
@@ -0,0 +1,478 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Chelsio Communications, Inc.
+ * Written by: John Baldwin <jhb@FreeBSD.org>
+ */
+
+#include <sys/param.h>
+#include <sys/linker.h>
+#include <sys/module.h>
+#include <sys/time.h>
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <libiscsiutil.h>
+#include <libnvmf.h>
+#include <libutil.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <cam/ctl/ctl.h>
+#include <cam/ctl/ctl_io.h>
+#include <cam/ctl/ctl_ioctl.h>
+
+#include <memory>
+
+#include "ctld.hh"
+#include "nvmf.hh"
+
+#define DEFAULT_MAXH2CDATA (256 * 1024)
+
+struct nvmf_io_portal final : public nvmf_portal {
+ nvmf_io_portal(struct portal_group *pg, const char *listen,
+ portal_protocol protocol, freebsd::addrinfo_up ai,
+ const struct nvmf_association_params &aparams,
+ nvmf_association_up na) :
+ nvmf_portal(pg, listen, protocol, std::move(ai), aparams,
+ std::move(na)) {}
+
+ void handle_connection(freebsd::fd_up fd, const char *host,
+ const struct sockaddr *client_sa) override;
+};
+
+struct nvmf_transport_group final : public portal_group {
+ nvmf_transport_group(struct conf *conf, std::string_view name) :
+ portal_group(conf, name) {}
+
+ const char *keyword() const override
+ { return "transport-group"; }
+
+ void allocate_tag() override;
+ bool add_portal(const char *value, portal_protocol protocol)
+ override;
+ void add_default_portals() override;
+ bool set_filter(const char *str) override;
+
+ virtual port_up create_port(struct target *target, auth_group_sp ag)
+ override;
+ virtual port_up create_port(struct target *target, uint32_t ctl_port)
+ override;
+
+private:
+ struct nvmf_association_params init_aparams(portal_protocol protocol);
+
+ static uint16_t last_port_id;
+};
+
+struct nvmf_port final : public portal_group_port {
+ nvmf_port(struct target *target, struct portal_group *pg,
+ auth_group_sp ag) :
+ portal_group_port(target, pg, ag) {}
+ nvmf_port(struct target *target, struct portal_group *pg,
+ uint32_t ctl_port) :
+ portal_group_port(target, pg, ctl_port) {}
+
+ bool kernel_create_port() override;
+ bool kernel_remove_port() override;
+
+private:
+ static bool modules_loaded;
+ static void load_kernel_modules();
+};
+
+struct nvmf_controller final : public target {
+ nvmf_controller(struct conf *conf, std::string_view name) :
+ target(conf, "controller", name) {}
+
+ bool add_host_nqn(std::string_view name) override;
+ bool add_host_address(const char *addr) override;
+ bool add_namespace(u_int id, const char *lun_name) override;
+ bool add_portal_group(const char *pg_name, const char *ag_name)
+ override;
+ struct lun *start_namespace(u_int id) override;
+
+protected:
+ struct portal_group *default_portal_group() override;
+};
+
+uint16_t nvmf_transport_group::last_port_id = 0;
+bool nvmf_port::modules_loaded = false;
+
+static bool need_tcp_transport = false;
+
+static bool
+parse_bool(const nvlist_t *nvl, const char *key, bool def)
+{
+ const char *value;
+
+ if (!nvlist_exists_string(nvl, key))
+ return def;
+
+ value = nvlist_get_string(nvl, key);
+ if (strcasecmp(value, "true") == 0 ||
+ strcasecmp(value, "1") == 0)
+ return true;
+ if (strcasecmp(value, "false") == 0 ||
+ strcasecmp(value, "0") == 0)
+ return false;
+
+ log_warnx("Invalid value \"%s\" for boolean option %s", value, key);
+ return def;
+}
+
+static uint64_t
+parse_number(const nvlist_t *nvl, const char *key, uint64_t def, uint64_t minv,
+ uint64_t maxv)
+{
+ const char *value;
+ int64_t val;
+
+ if (!nvlist_exists_string(nvl, key))
+ return def;
+
+ value = nvlist_get_string(nvl, key);
+ if (expand_number(value, &val) == 0 && val >= 0 &&
+ (uint64_t)val >= minv && (uint64_t)val <= maxv)
+ return (uint64_t)val;
+
+ log_warnx("Invalid value \"%s\" for numeric option %s", value, key);
+ return def;
+}
+
+struct nvmf_association_params
+nvmf_transport_group::init_aparams(portal_protocol protocol)
+{
+ struct nvmf_association_params params;
+ memset(&params, 0, sizeof(params));
+
+ /* Options shared between discovery and I/O associations. */
+ const nvlist_t *nvl = pg_options.get();
+ params.tcp.header_digests = parse_bool(nvl, "HDGST", false);
+ params.tcp.data_digests = parse_bool(nvl, "DDGST", false);
+ uint64_t value = parse_number(nvl, "MAXH2CDATA", DEFAULT_MAXH2CDATA,
+ 4096, UINT32_MAX);
+ if (value % 4 != 0) {
+ log_warnx("Invalid value \"%ju\" for option MAXH2CDATA",
+ (uintmax_t)value);
+ value = DEFAULT_MAXH2CDATA;
+ }
+ params.tcp.maxh2cdata = value;
+
+ switch (protocol) {
+ case portal_protocol::NVME_TCP:
+ params.sq_flow_control = parse_bool(nvl, "SQFC", false);
+ params.dynamic_controller_model = true;
+ params.max_admin_qsize = parse_number(nvl, "max_admin_qsize",
+ NVME_MAX_ADMIN_ENTRIES, NVME_MIN_ADMIN_ENTRIES,
+ NVME_MAX_ADMIN_ENTRIES);
+ params.max_io_qsize = parse_number(nvl, "max_io_qsize",
+ NVME_MAX_IO_ENTRIES, NVME_MIN_IO_ENTRIES,
+ NVME_MAX_IO_ENTRIES);
+ params.tcp.pda = 0;
+ break;
+ case portal_protocol::NVME_DISCOVERY_TCP:
+ params.sq_flow_control = false;
+ params.dynamic_controller_model = true;
+ params.max_admin_qsize = NVME_MAX_ADMIN_ENTRIES;
+ params.tcp.pda = 0;
+ break;
+ default:
+ __assert_unreachable();
+ }
+
+ return params;
+}
+
+portal_group_up
+nvmf_make_transport_group(struct conf *conf, std::string_view name)
+{
+ return std::make_unique<nvmf_transport_group>(conf, name);
+}
+
+target_up
+nvmf_make_controller(struct conf *conf, std::string_view name)
+{
+ return std::make_unique<nvmf_controller>(conf, name);
+}
+
+void
+nvmf_transport_group::allocate_tag()
+{
+ set_tag(++last_port_id);
+}
+
+bool
+nvmf_transport_group::add_portal(const char *value, portal_protocol protocol)
+{
+ freebsd::addrinfo_up ai;
+ enum nvmf_trtype trtype;
+
+ switch (protocol) {
+ case portal_protocol::NVME_TCP:
+ trtype = NVMF_TRTYPE_TCP;
+ ai = parse_addr_port(value, "4420");
+ break;
+ case portal_protocol::NVME_DISCOVERY_TCP:
+ trtype = NVMF_TRTYPE_TCP;
+ ai = parse_addr_port(value, "8009");
+ break;
+ default:
+ log_warnx("unsupported transport protocol for %s", value);
+ return false;
+ }
+
+ if (!ai) {
+ log_warnx("invalid listen address %s", value);
+ return false;
+ }
+
+ struct nvmf_association_params aparams = init_aparams(protocol);
+ nvmf_association_up association(nvmf_allocate_association(trtype, true,
+ &aparams));
+ if (!association) {
+ log_warn("Failed to create NVMe controller association");
+ return false;
+ }
+
+ /*
+ * XXX: getaddrinfo(3) may return multiple addresses; we should turn
+ * those into multiple portals.
+ */
+
+ portal_up portal;
+ if (protocol == portal_protocol::NVME_DISCOVERY_TCP) {
+ portal = std::make_unique<nvmf_discovery_portal>(this, value,
+ protocol, std::move(ai), aparams, std::move(association));
+ } else {
+ portal = std::make_unique<nvmf_io_portal>(this, value,
+ protocol, std::move(ai), aparams, std::move(association));
+ need_tcp_transport = true;
+ }
+
+ pg_portals.emplace_back(std::move(portal));
+ return true;
+}
+
+void
+nvmf_transport_group::add_default_portals()
+{
+ add_portal("0.0.0.0", portal_protocol::NVME_DISCOVERY_TCP);
+ add_portal("[::]", portal_protocol::NVME_DISCOVERY_TCP);
+ add_portal("0.0.0.0", portal_protocol::NVME_TCP);
+ add_portal("[::]", portal_protocol::NVME_TCP);
+}
+
+bool
+nvmf_transport_group::set_filter(const char *str)
+{
+ enum discovery_filter filter;
+
+ if (strcmp(str, "none") == 0) {
+ filter = discovery_filter::NONE;
+ } else if (strcmp(str, "address") == 0) {
+ filter = discovery_filter::PORTAL;
+ } else if (strcmp(str, "address-name") == 0) {
+ filter = discovery_filter::PORTAL_NAME;
+ } else {
+ log_warnx("invalid discovery-filter \"%s\" for transport-group "
+ "\"%s\"; valid values are \"none\", \"address\", "
+ "and \"address-name\"",
+ str, name());
+ return false;
+ }
+
+ if (pg_discovery_filter != discovery_filter::UNKNOWN &&
+ pg_discovery_filter != filter) {
+ log_warnx("cannot set discovery-filter to \"%s\" for "
+ "transport-group \"%s\"; already has a different "
+ "value", str, name());
+ return false;
+ }
+
+ pg_discovery_filter = filter;
+ return true;
+}
+
+port_up
+nvmf_transport_group::create_port(struct target *target, auth_group_sp ag)
+{
+ return std::make_unique<nvmf_port>(target, this, ag);
+}
+
+port_up
+nvmf_transport_group::create_port(struct target *target, uint32_t ctl_port)
+{
+ return std::make_unique<nvmf_port>(target, this, ctl_port);
+}
+
+void
+nvmf_port::load_kernel_modules()
+{
+ int saved_errno;
+
+ if (modules_loaded)
+ return;
+
+ saved_errno = errno;
+ if (modfind("nvmft") == -1 && kldload("nvmft") == -1)
+ log_warn("couldn't load nvmft");
+
+ if (need_tcp_transport) {
+ if (modfind("nvmf/tcp") == -1 && kldload("nvmf_tcp") == -1)
+ log_warn("couldn't load nvmf_tcp");
+ }
+
+ errno = saved_errno;
+ modules_loaded = true;
+}
+
+bool
+nvmf_port::kernel_create_port()
+{
+ struct portal_group *pg = p_portal_group;
+ struct target *targ = p_target;
+
+ load_kernel_modules();
+
+ freebsd::nvlist_up nvl = pg->options();
+ nvlist_add_string(nvl.get(), "subnqn", targ->name());
+ nvlist_add_string(nvl.get(), "ctld_transport_group_name",
+ pg->name());
+ nvlist_add_stringf(nvl.get(), "portid", "%u", pg->tag());
+ if (!nvlist_exists_string(nvl.get(), "max_io_qsize"))
+ nvlist_add_stringf(nvl.get(), "max_io_qsize", "%u",
+ NVME_MAX_IO_ENTRIES);
+
+ return ctl_create_port("nvmf", nvl.get(), &p_ctl_port);
+}
+
+bool
+nvmf_port::kernel_remove_port()
+{
+ freebsd::nvlist_up nvl(nvlist_create(0));
+ nvlist_add_string(nvl.get(), "subnqn", p_target->name());
+
+ return ctl_remove_port("nvmf", nvl.get());
+}
+
+bool
+nvmf_controller::add_host_nqn(std::string_view name)
+{
+ if (!use_private_auth("host-nqn"))
+ return false;
+ return t_auth_group->add_host_nqn(name);
+}
+
+bool
+nvmf_controller::add_host_address(const char *addr)
+{
+ if (!use_private_auth("host-address"))
+ return false;
+ return t_auth_group->add_host_address(addr);
+}
+
+bool
+nvmf_controller::add_namespace(u_int id, const char *lun_name)
+{
+ if (id == 0) {
+ log_warnx("namespace ID cannot be 0 for %s", label());
+ return false;
+ }
+
+ std::string lun_label = "namespace ID " + std::to_string(id - 1);
+ return target::add_lun(id, lun_label.c_str(), lun_name);
+}
+
+bool
+nvmf_controller::add_portal_group(const char *pg_name, const char *ag_name)
+{
+ struct portal_group *pg;
+ auth_group_sp ag;
+
+ pg = t_conf->find_transport_group(pg_name);
+ if (pg == NULL) {
+ log_warnx("unknown transport-group \"%s\" for %s", pg_name,
+ label());
+ return false;
+ }
+
+ if (ag_name != NULL) {
+ ag = t_conf->find_auth_group(ag_name);
+ if (ag == NULL) {
+ log_warnx("unknown auth-group \"%s\" for %s", ag_name,
+ label());
+ return false;
+ }
+ }
+
+ if (!t_conf->add_port(this, pg, std::move(ag))) {
+ log_warnx("can't link transport-group \"%s\" to %s", pg_name,
+ label());
+ return false;
+ }
+ return true;
+}
+
+struct lun *
+nvmf_controller::start_namespace(u_int id)
+{
+ if (id == 0) {
+ log_warnx("namespace ID cannot be 0 for %s", label());
+ return nullptr;
+ }
+
+ std::string lun_label = "namespace ID " + std::to_string(id - 1);
+ std::string lun_name = freebsd::stringf("%s,nsid,%u", name(), id);
+ return target::start_lun(id, lun_label.c_str(), lun_name.c_str());
+}
+
+struct portal_group *
+nvmf_controller::default_portal_group()
+{
+ return t_conf->find_transport_group("default");
+}
+
+void
+nvmf_io_portal::handle_connection(freebsd::fd_up fd, const char *host __unused,
+ const struct sockaddr *client_sa __unused)
+{
+ struct nvmf_qpair_params qparams;
+ memset(&qparams, 0, sizeof(qparams));
+ qparams.tcp.fd = fd;
+
+ struct nvmf_capsule *nc = NULL;
+ struct nvmf_fabric_connect_data data;
+ nvmf_qpair_up qp(nvmf_accept(association(), &qparams, &nc, &data));
+ if (!qp) {
+ log_warnx("Failed to create NVMe I/O qpair: %s",
+ nvmf_association_error(association()));
+ return;
+ }
+ nvmf_capsule_up nc_guard(nc);
+ const struct nvmf_fabric_connect_cmd *cmd =
+ (const struct nvmf_fabric_connect_cmd *)nvmf_capsule_sqe(nc);
+
+ struct ctl_nvmf req;
+ memset(&req, 0, sizeof(req));
+ req.type = CTL_NVMF_HANDOFF;
+ int error = nvmf_handoff_controller_qpair(qp.get(), cmd, &data,
+ &req.data.handoff);
+ if (error != 0) {
+ log_warnc(error,
+ "Failed to prepare NVMe I/O qpair for handoff");
+ return;
+ }
+
+ if (ioctl(ctl_fd, CTL_NVMF, &req) != 0)
+ log_warn("ioctl(CTL_NVMF/CTL_NVMF_HANDOFF)");
+ if (req.status == CTL_NVMF_ERROR)
+ log_warnx("Failed to handoff NVMF connection: %s",
+ req.error_str);
+ else if (req.status != CTL_NVMF_OK)
+ log_warnx("Failed to handoff NVMF connection with status %d",
+ req.status);
+}