diff options
| author | John Baldwin <jhb@FreeBSD.org> | 2025-08-06 19:57:50 +0000 |
|---|---|---|
| committer | John Baldwin <jhb@FreeBSD.org> | 2025-08-06 19:59:13 +0000 |
| commit | 66b5296f1b29083634e2875ff08c32e7b6b866a8 (patch) | |
| tree | 4018f2b3665ae4470718ac9228cb7cade5166c1f | |
| parent | 40484d3117d9520de3755d5c91ddb26ed7ce6bcb (diff) | |
ctld: Add support for NVMe over Fabrics
While the overall structure is similar for NVMeoF controllers and
iSCSI targets, there are sufficient differences that NVMe support uses
an alternate configuration syntax.
- In authentication groups, permitted NVMeoF hosts can be allowed by
names (NQNs) via "host-nqn" values (similar to "initiator-name" for
iSCSI). Similarly, "host-address" accepts permitted host addresses
similar to "initiator-portal" for iSCSI.
- A new "transport-group" context enumerates transports that can be
used by a group of NVMeoF controllers similar to the "portal-group"
context for iSCSI. In this section, the "listen" keyword accepts a
transport as well as an address to permit other types of transports
besides TCP in the future. The "foreign", "offload", and "redirect"
keywords are also not meaningful and thus not supported.
- A new "controller" context describes an NVMeoF I/O controller
similar to the "target" context for iSCSI. One key difference here
is that "lun" objects are replaced by "namespace" objects. However,
a "namespace" can reference a named global lun permitting LUNs to be
shared between iSCSI targets and NVMeoF controllers.
NB: Authentication via CHAP is not implemented for NVMeoF.
Reviewed by: imp
Sponsored by: Chelsio Communications
Differential Revision: https://reviews.freebsd.org/D48773
| -rw-r--r-- | usr.sbin/ctld/Makefile | 6 | ||||
| -rw-r--r-- | usr.sbin/ctld/conf.cc | 67 | ||||
| -rw-r--r-- | usr.sbin/ctld/conf.h | 12 | ||||
| -rw-r--r-- | usr.sbin/ctld/ctl.conf.5 | 242 | ||||
| -rw-r--r-- | usr.sbin/ctld/ctld.cc | 166 | ||||
| -rw-r--r-- | usr.sbin/ctld/ctld.hh | 43 | ||||
| -rw-r--r-- | usr.sbin/ctld/discovery.cc | 3 | ||||
| -rw-r--r-- | usr.sbin/ctld/kernel.cc | 54 | ||||
| -rw-r--r-- | usr.sbin/ctld/nvmf.cc | 478 | ||||
| -rw-r--r-- | usr.sbin/ctld/nvmf.hh | 71 | ||||
| -rw-r--r-- | usr.sbin/ctld/nvmf_discovery.cc | 518 | ||||
| -rw-r--r-- | usr.sbin/ctld/parse.y | 232 | ||||
| -rw-r--r-- | usr.sbin/ctld/token.l | 7 | ||||
| -rw-r--r-- | usr.sbin/ctld/uclparse.cc | 381 |
14 files changed, 2263 insertions, 17 deletions
diff --git a/usr.sbin/ctld/Makefile b/usr.sbin/ctld/Makefile index ad3a2661794a..61efe8a05cfb 100644 --- a/usr.sbin/ctld/Makefile +++ b/usr.sbin/ctld/Makefile @@ -6,19 +6,21 @@ CFLAGS+=-I${SRCTOP}/contrib/libucl/include PACKAGE= ctl PROG_CXX= ctld SRCS= ctld.cc conf.cc discovery.cc iscsi.cc isns.cc kernel.cc -SRCS+= login.cc parse.y token.l y.tab.h uclparse.cc +SRCS+= login.cc nvmf.cc nvmf_discovery.cc +SRCS+= parse.y token.l y.tab.h uclparse.cc CFLAGS+= -I${.CURDIR} CFLAGS+= -I${SRCTOP}/sys CFLAGS+= -I${SRCTOP}/sys/cam/ctl CFLAGS+= -I${SRCTOP}/sys/dev/iscsi CFLAGS+= -I${SRCTOP}/lib/libiscsiutil CFLAGS+= -I${SRCTOP}/lib/libutil++ +CFLAGS+= -I${SRCTOP}/lib/libnvmf #CFLAGS+= -DICL_KERNEL_PROXY NO_WCAST_ALIGN= CXXWARNFLAGS.gcc= -Wno-shadow MAN= ctld.8 ctl.conf.5 -LIBADD= bsdxml iscsiutil md sbuf util ucl m nv util++ +LIBADD= bsdxml iscsiutil nvmf md sbuf util ucl m nv util++ YFLAGS+= -v CLEANFILES= y.tab.c y.tab.h y.output diff --git a/usr.sbin/ctld/conf.cc b/usr.sbin/ctld/conf.cc index d8f941e0bc52..ab76f8e2ed0b 100644 --- a/usr.sbin/ctld/conf.cc +++ b/usr.sbin/ctld/conf.cc @@ -123,6 +123,18 @@ auth_group_add_chap_mutual(const char *user, const char *secret, } bool +auth_group_add_host_address(const char *portal) +{ + return (auth_group->add_host_address(portal)); +} + +bool +auth_group_add_host_nqn(const char *name) +{ + return (auth_group->add_host_nqn(name)); +} + +bool auth_group_add_initiator_name(const char *name) { return (auth_group->add_initiator_name(name)); @@ -234,6 +246,29 @@ portal_group_set_tag(uint16_t tag) } bool +transport_group_start(const char *name) +{ + if (strcmp(name, "default") == 0) + portal_group = conf->define_default_transport_group(); + else + portal_group = conf->add_transport_group(name); + return (portal_group != NULL); +} + +bool +transport_group_add_listen_discovery_tcp(const char *listen) +{ + return portal_group->add_portal(listen, + portal_protocol::NVME_DISCOVERY_TCP); +} + +bool +transport_group_add_listen_tcp(const char *listen) +{ + return portal_group->add_portal(listen, portal_protocol::NVME_TCP); +} + +bool lun_start(const char *name) { lun = conf->add_lun(name); @@ -388,6 +423,38 @@ target_start_lun(u_int id) } bool +controller_start(const char *name) +{ + target = conf->add_controller(name); + return (target != nullptr); +} + +bool +controller_add_host_address(const char *addr) +{ + return (target->add_host_address(addr)); +} + +bool +controller_add_host_nqn(const char *name) +{ + return (target->add_host_nqn(name)); +} + +bool +controller_add_namespace(u_int id, const char *name) +{ + return (target->add_namespace(id, name)); +} + +bool +controller_start_namespace(u_int id) +{ + lun = target->start_namespace(id); + return (lun != nullptr); +} + +bool parse_conf(const char *path) { freebsd::FILE_up fp(fopen(path, "r")); diff --git a/usr.sbin/ctld/conf.h b/usr.sbin/ctld/conf.h index b13fd80e9fe5..642c8f234d30 100644 --- a/usr.sbin/ctld/conf.h +++ b/usr.sbin/ctld/conf.h @@ -43,6 +43,8 @@ void auth_group_finish(void); bool auth_group_add_chap(const char *user, const char *secret); bool auth_group_add_chap_mutual(const char *user, const char *secret, const char *user2, const char *secret2); +bool auth_group_add_host_address(const char *portal); +bool auth_group_add_host_nqn(const char *name); bool auth_group_add_initiator_name(const char *name); bool auth_group_add_initiator_portal(const char *portal); bool auth_group_set_type(const char *type); @@ -69,6 +71,10 @@ bool portal_group_set_pcp(u_int pcp); bool portal_group_set_redirection(const char *addr); void portal_group_set_tag(uint16_t tag); +bool transport_group_start(const char *name); +bool transport_group_add_listen_discovery_tcp(const char *listen); +bool transport_group_add_listen_tcp(const char *listen); + bool target_start(const char *name); void target_finish(void); bool target_add_chap(const char *user, const char *secret); @@ -85,6 +91,12 @@ bool target_set_physical_port(const char *pport); bool target_set_redirection(const char *addr); bool target_start_lun(u_int id); +bool controller_start(const char *name); +bool controller_add_host_address(const char *addr); +bool controller_add_host_nqn(const char *name); +bool controller_add_namespace(u_int id, const char *name); +bool controller_start_namespace(u_int id); + bool lun_start(const char *name); void lun_finish(void); bool lun_add_option(const char *name, const char *value); diff --git a/usr.sbin/ctld/ctl.conf.5 b/usr.sbin/ctld/ctl.conf.5 index e42dd8067006..12f4186a6844 100644 --- a/usr.sbin/ctld/ctl.conf.5 +++ b/usr.sbin/ctld/ctl.conf.5 @@ -26,12 +26,12 @@ .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd February 26, 2025 +.Dd August 6, 2025 .Dt CTL.CONF 5 .Os .Sh NAME .Nm ctl.conf -.Nd CAM Target Layer / iSCSI target daemon configuration file +.Nd CAM Target Layer / iSCSI target / NVMeoF controller daemon configuration file .Sh DESCRIPTION The .Nm @@ -59,6 +59,11 @@ file is: .Dl ... } +.No transport-group Ar name No { +.Dl listen Ar transport Ar address +.Dl ... +} + .No target Ar name { .Dl auth-group Ar name .Dl portal-group Ar name @@ -67,6 +72,15 @@ file is: .Dl } .Dl ... } + +.No controller Ar name { +.Dl auth-group Ar name +.Dl transport-group Ar name +.Dl namespace Ar number No { +.Dl path Ar path +.Dl } +.Dl ... +} .Ed .Ss Global Context .Bl -tag -width indent @@ -94,16 +108,29 @@ Create a configuration context, defining a new portal-group, which can then be assigned to any number of targets. +.It Ic transport-group Ar name +Create a +.Sy transport-group +configuration context, +defining a new transport-group, +which can then be assigned to any number of NVMeoF controllers. .It Ic lun Ar name Create a .Sy lun -configuration context, defining a LUN to be exported by any number of targets. +configuration context, defining a LUN to be exported by any number of targets +or controllers. .It Ic target Ar name Create a .Sy target configuration context, which can optionally contain one or more .Sy lun contexts. +.It Ic controller Ar name +Create a +.Sy controller +configuration context, which can optionally contain one or more +.Sy namespace +contexts. .It Ic timeout Ar seconds The timeout for login sessions, after which the connection will be forcibly terminated. @@ -150,6 +177,19 @@ the configuration may only contain either or .Sy chap-mutual entries; it is an error to mix them. +.It Ic host-address Ar address Ns Op / Ns Ar prefixlen +An NVMeoF host address: an IPv4 or IPv6 address, optionally +followed by a literal slash and a prefix length. +Only NVMeoF hosts with an address matching one of the defined +addresses will be allowed to connect. +If not defined, there will be no restrictions based on host +address. +.It Ic host-nqn Ar name +An NVMeoF host name. +Only NVMeoF hosts with a name matching one of the defined +names will be allowed to connect. +If not defined, there will be no restrictions based on NVMe host +name. .It Ic initiator-name Ar initiator-name An iSCSI initiator name. Only initiators with a name matching one of the defined @@ -264,6 +304,75 @@ to .Qq Ar 7 . When omitted, the default for the outgoing interface is used. .El +.Ss transport-group Context +.Bl -tag -width indent +.It Ic discovery-auth-group Ar name +See the description for this option for +.Sy portal-group +contexts. +.It Ic discovery-filter Ar filter +Filter can be either +.Qq Ar none , +.Qq Ar address , +or +.Qq Ar address-name . +When set to +.Qq Ar none , +discovery will return all controllers assigned to that transport group. +When set to +.Qq Ar address , +discovery will not return controllers that cannot be accessed by the +host because of their +.Sy host-address . +When set to +.Qq Ar address-name , +the check will include both +.Sy host-address +and +.Sy host-nqn . +The default is +.Qq Ar none . +.It Ic listen Ar transport Ar address +An IPv4 or IPv6 address and port to listen on for incoming connections +using the specified NVMeoF transport. +Supported transports are +.Qq Ar tcp +.Pq for NVMe/TCP I/O controllers +and +.Qq Ar discovery-tcp +.Pq for NVMe/TCP discovery controllers . +.It Ic option Ar name Ar value +One of the following options: +.Bl -column "max_admin_qsize" "Default" "Transports" +.It Sy Name Ta Sy Default Ta Sy Transports Ta Sy Description +.It MAXH2CDATA Ta 256KiB Ta TCP Ta +Size in bytes of the maximum data payload size for data PDUs accepted from +remote hosts. +The value must be at least 4KiB and must be a multiple of 4. +.It SQFC Ta false Ta any Ta +Always enable SQ flow control. +.It HDGST Ta false Ta TCP Ta +Enable PDU header digests if requested by a remote host. +.It DDGST Ta false Ta TCP Ta +Enable PDU data digests if requested by a remote host. +.It max_admin_qsize Ta 4096 Ta any Ta +The maximum number of entries a remote host can request for an admin queue pair. +.It max_io_qsize Ta 65536 Ta any Ta +The maximum number of entries a remote host can request for an I/O queue pair. +.El +.It Ic tag Ar value +Unique 16-bit port ID for this +.Sy transport-group . +If not specified, the value is generated automatically. +.It Ic dscp Ar value +See the description for this option for +.Sy portal-group +contexts. +.It Ic pcp Ar value +See the description for this option for +.Sy portal-group +contexts. +.El .Ss target Context .Bl -tag -width indent .It Ic alias Ar text @@ -390,6 +499,101 @@ configuration context, defining a LUN exported by the parent target. This is an alternative to defining the LUN separately, useful in the common case of a LUN being exported by a single target. .El +.Ss controller Context +.Bl -tag -width indent +.It Ic auth-group Ar name +Assign a previously defined authentication group to the controller. +By default, controllers that do not specify their own auth settings, +using clauses such as +.Sy host-address +or +.Sy host-nqn , +are assigned to the +predefined +.Sy auth-group +.Qq Ar default , +which denies all access. +Another predefined +.Sy auth-group , +.Qq Ar no-authentication , +may be used to permit access +without authentication. +Note that this clause can be overridden using the second argument +to a +.Sy transport-group +clause. +.It Ic auth-type Ar type +Sets the authentication type. +Type can be either +.Qq Ar none +or +.Qq Ar deny . +In most cases it is not necessary to set the type using this clause; +it is usually used to disable authentication for a given +.Sy controller . +This clause is mutually exclusive with +.Sy auth-group ; +one cannot use +both in a single controller. +.It Ic host-address Ar address Ns Op / Ns Ar prefixlen +An NVMeoF host address: an IPv4 or IPv6 address, optionally +followed by a literal slash and a prefix length. +Only NVMeoF hosts with an address matching one of the defined +addresses will be allowed to connect. +If not defined, there will be no restrictions based on host +address. +This clause is mutually exclusive with +.Sy auth-group ; +one cannot use +both in a single controller. +.It Ic host-nqn Ar name +An NVMeoF host name. +Only NVMeoF hosts with a name matching one of the defined +names will be allowed to connect. +If not defined, there will be no restrictions based on NVMe host +name. +This clause is mutually exclusive with +.Sy auth-group ; +one cannot use +both in a single target. +.Pp +The +.Sy auth-type , +.Sy host-address , +and +.Sy host-nqn +clauses in the controller context provide an alternative to assigning an +.Sy auth-group +defined separately, useful in the common case of authentication settings +specific to a single controller. +.It Ic transport-group Ar name Op Ar ag-name +Assign a previously defined transport group to the controller. +The default transport group is +.Qq Ar default , +which makes the controller available +on TCP port 4420 on all configured IPv4 and IPv6 addresses. +The optional second argument specifies the +.Sy auth-group +for connections to this specific transport group group. +If the second argument is not specified, the controller +.Sy auth-group +is used. +.It Ic namespace Ar number Ar name +Export previously defined +.Sy lun +as an NVMe namespace from the parent controller. +.It Ic namespace Ar number +Create a +.Sy namespace +configuration context, defining an NVMe namespace exported by the parent target. +.Pp +This is an alternative to defining the namespace separately, +useful in the common case of a namespace being exported by a single controller. +.Sy namespace +configuration contexts accept the the same properties as +.Sy lun +contexts. +.El .Ss lun Context .Bl -tag -width indent .It Ic backend Ar block No | Ar ramdisk @@ -410,7 +614,7 @@ Global numeric identifier to use for a given LUN inside CTL. By default CTL allocates those IDs dynamically, but explicit specification may be needed for consistency in HA configurations. .It Ic device-id Ar string -The SCSI Device Identification string presented to the initiator. +The SCSI Device Identification string presented to iSCSI initiators. .It Ic device-type Ar type Specify the SCSI device type to use when creating the LUN. Currently CTL supports Direct Access (type 0), Processor (type 3) @@ -425,11 +629,11 @@ section of The path to the file, device node, or .Xr zfs 8 volume used to back the LUN. -For optimal performance, create the volume with the +For optimal performance, create ZFS volumes with the .Qq Ar volmode=dev property set. .It Ic serial Ar string -The SCSI serial number presented to the initiator. +The SCSI serial number presented to iSCSI initiators. .It Ic size Ar size The LUN size, in bytes or by number with a suffix of .Sy K , M , G , T @@ -498,6 +702,16 @@ target naa.50015178f369f092 { port isp1 lun 0 example_1 } + +controller nqn.2012-06.com.example:controller1 { + auth-group no-authentication; + namespace 1 example_1 + namespace 2 { + backend ramdisk + size 1G + option capacity 1G + } +} .Ed .Pp An equivalent configuration in UCL format, for use with @@ -585,6 +799,22 @@ target { } } } + +controller { + "nqn.2012-06.com.example:controller1" { + auth-group = no-authentication + namespace = { + 1 = example_1, + 2 { + backend = ramdisk + size = 1G + options { + capacity = 1G + } + } + } + } +} .Ed .Sh SEE ALSO .Xr ctl 4 , diff --git a/usr.sbin/ctld/ctld.cc b/usr.sbin/ctld/ctld.cc index 451245b8d5fa..10c12f25068e 100644 --- a/usr.sbin/ctld/ctld.cc +++ b/usr.sbin/ctld/ctld.cc @@ -41,6 +41,7 @@ #include <assert.h> #include <ctype.h> #include <errno.h> +#include <libnvmf.h> #include <netdb.h> #include <signal.h> #include <stdbool.h> @@ -67,6 +68,8 @@ static volatile bool sigalrm_received = false; static int kqfd; static int nchildren = 0; +uint32_t conf::global_genctr; + static void usage(void) { @@ -76,6 +79,11 @@ usage(void) exit(1); } +conf::conf() +{ + conf_genctr = global_genctr++; +} + void conf::set_debug(int debug) { @@ -278,6 +286,23 @@ auth_group::add_chap_mutual(const char *user, const char *secret, } bool +auth_group::add_host_nqn(std::string_view nqn) +{ + /* Silently ignore duplicates. */ + ag_host_names.emplace(nqn); + return (true); +} + +bool +auth_group::host_permitted(std::string_view nqn) const +{ + if (ag_host_names.empty()) + return (true); + + return (ag_host_names.count(std::string(nqn)) != 0); +} + +bool auth_group::add_initiator_name(std::string_view name) { /* Silently ignore duplicates. */ @@ -361,6 +386,20 @@ auth_portal::parse(const char *portal) } bool +auth_group::add_host_address(const char *address) +{ + auth_portal ap; + if (!ap.parse(address)) { + log_warnx("invalid controller address \"%s\" for %s", address, + label()); + return (false); + } + + ag_host_addresses.emplace_back(ap); + return (true); +} + +bool auth_group::add_initiator_portal(const char *portal) { auth_portal ap; @@ -407,6 +446,18 @@ auth_portal::matches(const struct sockaddr *sa) const } bool +auth_group::host_permitted(const struct sockaddr *sa) const +{ + if (ag_host_addresses.empty()) + return (true); + + for (const auth_portal &ap : ag_host_addresses) + if (ap.matches(sa)) + return (true); + return (false); +} + +bool auth_group::initiator_permitted(const struct sockaddr *sa) const { if (ag_initiator_portals.empty()) @@ -501,6 +552,45 @@ conf::find_portal_group(std::string_view name) return (it->second.get()); } +struct portal_group * +conf::add_transport_group(const char *name) +{ + auto pair = conf_transport_groups.try_emplace(name, + nvmf_make_transport_group(this, name)); + if (!pair.second) { + log_warnx("duplicated transport-group \"%s\"", name); + return (nullptr); + } + + return (pair.first->second.get()); +} + +/* + * Make it possible to redefine the default transport-group, but only + * once. + */ +struct portal_group * +conf::define_default_transport_group() +{ + if (conf_default_tg_defined) { + log_warnx("duplicated transport-group \"default\""); + return (nullptr); + } + + conf_default_tg_defined = true; + return (find_transport_group("default")); +} + +struct portal_group * +conf::find_transport_group(std::string_view name) +{ + auto it = conf_transport_groups.find(std::string(name)); + if (it == conf_transport_groups.end()) + return (nullptr); + + return (it->second.get()); +} + bool portal_group::is_dummy() const { @@ -1113,6 +1203,40 @@ portal_group::find_port(std::string_view target) const return (it->second); } +struct target * +conf::add_controller(const char *name) +{ + if (!nvmf_nqn_valid_strict(name)) { + log_warnx("controller name \"%s\" is invalid for NVMe", name); + return nullptr; + } + + /* + * Normalize the name to lowercase to match iSCSI. + */ + std::string t_name(name); + for (char &c : t_name) + c = tolower(c); + + auto const &pair = conf_controllers.try_emplace(t_name, + nvmf_make_controller(this, t_name)); + if (!pair.second) { + log_warnx("duplicated controller \"%s\"", name); + return nullptr; + } + + return pair.first->second.get(); +} + +struct target * +conf::find_controller(std::string_view name) +{ + auto it = conf_controllers.find(std::string(name)); + if (it == conf_controllers.end()) + return nullptr; + return it->second.get(); +} + target::target(struct conf *conf, const char *keyword, std::string_view name) : t_conf(conf), t_name(name) { @@ -1367,6 +1491,8 @@ conf::delete_target_luns(struct lun *lun) { for (const auto &kv : conf_targets) kv.second->remove_lun(lun); + for (const auto &kv : conf_controllers) + kv.second->remove_lun(lun); } struct lun * @@ -1667,9 +1793,15 @@ conf::verify() for (auto &kv : conf_targets) { kv.second->verify(); } + for (auto &kv : conf_controllers) { + kv.second->verify(); + } for (auto &kv : conf_portal_groups) { kv.second->verify(this); } + for (auto &kv : conf_transport_groups) { + kv.second->verify(this); + } for (const auto &kv : conf_auth_groups) { const std::string &ag_name = kv.first; if (ag_name == "default" || @@ -1813,6 +1945,12 @@ conf::reuse_portal_group_socket(struct portal &newp) if (pg.reuse_socket(newp)) return (true); } + for (auto &kv : conf_transport_groups) { + struct portal_group &pg = *kv.second; + + if (pg.reuse_socket(newp)) + return (true); + } return (false); } @@ -1864,6 +2002,17 @@ conf::apply(struct conf *oldconf) else newpg.allocate_tag(); } + for (auto &kv : conf_transport_groups) { + struct portal_group &newpg = *kv.second; + + if (newpg.tag() != 0) + continue; + auto it = oldconf->conf_transport_groups.find(kv.first); + if (it != oldconf->conf_transport_groups.end()) + newpg.set_tag(it->second->tag()); + else + newpg.allocate_tag(); + } /* Deregister on removed iSNS servers. */ for (auto &kv : oldconf->conf_isns) { @@ -2027,6 +2176,9 @@ conf::apply(struct conf *oldconf) for (auto &kv : conf_portal_groups) { cumulated_error += kv.second->open_sockets(*oldconf); } + for (auto &kv : conf_transport_groups) { + cumulated_error += kv.second->open_sockets(*oldconf); + } /* * Go through the no longer used sockets, closing them. @@ -2034,6 +2186,9 @@ conf::apply(struct conf *oldconf) for (auto &kv : oldconf->conf_portal_groups) { kv.second->close_sockets(); } + for (auto &kv : oldconf->conf_transport_groups) { + kv.second->close_sockets(); + } /* (Re-)Register on remaining/new iSNS servers. */ for (auto &kv : conf_isns) { @@ -2397,6 +2552,9 @@ conf_new_from_file(const char *path, bool ucl) pg = conf->add_portal_group("default"); assert(pg != NULL); + pg = conf->add_transport_group("default"); + assert(pg != NULL); + conf_start(conf.get()); if (ucl) valid = uclparse_conf(path); @@ -2427,6 +2585,14 @@ conf_new_from_file(const char *path, bool ucl) pg->add_default_portals(); } + if (!conf->default_portal_group_defined()) { + log_debugx("transport-group \"default\" not defined; " + "going with defaults"); + pg = conf->find_transport_group("default"); + assert(pg != NULL); + pg->add_default_portals(); + } + if (!conf->verify()) { conf.reset(); return {}; diff --git a/usr.sbin/ctld/ctld.hh b/usr.sbin/ctld/ctld.hh index 6ecee3b73c4f..a5aab65e339b 100644 --- a/usr.sbin/ctld/ctld.hh +++ b/usr.sbin/ctld/ctld.hh @@ -110,6 +110,12 @@ struct auth_group { const char *user2, const char *secret2); const struct auth *find_auth(std::string_view user) const; + bool add_host_nqn(std::string_view nqn); + bool host_permitted(std::string_view nqn) const; + + bool add_host_address(const char *address); + bool host_permitted(const struct sockaddr *sa) const; + bool add_initiator_name(std::string_view initiator_name); bool initiator_permitted(std::string_view initiator_name) const; @@ -123,6 +129,8 @@ private: std::string ag_label; auth_type ag_type = auth_type::UNKNOWN; std::unordered_map<std::string, auth> ag_auths; + std::unordered_set<std::string> ag_host_names; + std::list<auth_portal> ag_host_addresses; std::unordered_set<std::string> ag_initiator_names; std::list<auth_portal> ag_initiator_portals; }; @@ -131,7 +139,9 @@ using auth_group_sp = std::shared_ptr<auth_group>; enum class portal_protocol { ISCSI, - ISER + ISER, + NVME_TCP, + NVME_DISCOVERY_TCP, }; struct portal { @@ -147,7 +157,7 @@ struct portal { virtual void handle_connection(freebsd::fd_up fd, const char *host, const struct sockaddr *client_sa) = 0; - portal_group *portal_group() { return p_portal_group; } + portal_group *portal_group() const { return p_portal_group; } const char *listen() const { return p_listen.c_str(); } const addrinfo *ai() const { return p_ai.get(); } portal_protocol protocol() const { return p_protocol; } @@ -386,9 +396,12 @@ struct target { bool add_chap(const char *user, const char *secret); bool add_chap_mutual(const char *user, const char *secret, const char *user2, const char *secret2); + virtual bool add_host_address(const char *) { return false; } + virtual bool add_host_nqn(std::string_view) { return false; } virtual bool add_initiator_name(std::string_view) { return false; } virtual bool add_initiator_portal(const char *) { return false; } virtual bool add_lun(u_int, const char *) { return false; } + virtual bool add_namespace(u_int, const char *) { return false; } virtual bool add_portal_group(const char *pg_name, const char *ag_name) = 0; bool set_alias(std::string_view alias); @@ -397,6 +410,7 @@ struct target { bool set_physical_port(std::string_view pport); bool set_redirection(const char *addr); virtual struct lun *start_lun(u_int) { return nullptr; } + virtual struct lun *start_namespace(u_int) { return nullptr; } void add_port(struct port *port); void remove_lun(struct lun *lun); @@ -440,13 +454,18 @@ private: }; struct conf { + conf(); + int maxproc() const { return conf_maxproc; } int timeout() const { return conf_timeout; } + uint32_t genctr() const { return conf_genctr; } bool default_auth_group_defined() const { return conf_default_ag_defined; } bool default_portal_group_defined() const { return conf_default_pg_defined; } + bool default_transport_group_defined() const + { return conf_default_tg_defined; } struct auth_group *add_auth_group(const char *ag_name); struct auth_group *define_default_auth_group(); @@ -456,6 +475,10 @@ struct conf { struct portal_group *define_default_portal_group(); struct portal_group *find_portal_group(std::string_view name); + struct portal_group *add_transport_group(const char *name); + struct portal_group *define_default_transport_group(); + struct portal_group *find_transport_group(std::string_view name); + bool add_port(struct target *target, struct portal_group *pg, auth_group_sp ag); bool add_port(struct target *target, struct portal_group *pg, @@ -465,6 +488,9 @@ struct conf { int vp); bool add_pports(struct kports &kports); + struct target *add_controller(const char *name); + struct target *find_controller(std::string_view name); + struct target *add_target(const char *name); struct target *find_target(std::string_view name); @@ -501,10 +527,12 @@ private: std::string conf_pidfile_path; std::unordered_map<std::string, std::unique_ptr<lun>> conf_luns; - std::unordered_map<std::string, std::unique_ptr<target>> conf_targets; + std::unordered_map<std::string, target_up> conf_targets; + std::unordered_map<std::string, target_up> conf_controllers; std::unordered_map<std::string, auth_group_sp> conf_auth_groups; std::unordered_map<std::string, std::unique_ptr<port>> conf_ports; std::unordered_map<std::string, portal_group_up> conf_portal_groups; + std::unordered_map<std::string, portal_group_up> conf_transport_groups; std::unordered_map<std::string, isns> conf_isns; struct target *conf_first_target = nullptr; int conf_isns_period = 900; @@ -512,12 +540,16 @@ private: int conf_debug = 0; int conf_timeout = 60; int conf_maxproc = 30; + uint32_t conf_genctr = 0; freebsd::pidfile conf_pidfile; bool conf_default_pg_defined = false; + bool conf_default_tg_defined = false; bool conf_default_ag_defined = false; + static uint32_t global_genctr; + #ifdef ICL_KERNEL_PROXY public: int add_proxy_portal(portal *); @@ -593,6 +625,11 @@ portal_group_up iscsi_make_portal_group(struct conf *conf, target_up iscsi_make_target(struct conf *conf, std::string_view name); +portal_group_up nvmf_make_transport_group(struct conf *conf, + std::string_view name); +target_up nvmf_make_controller(struct conf *conf, + std::string_view name); + void start_timer(int timeout, bool fatal = false); void stop_timer(); bool timed_out(); diff --git a/usr.sbin/ctld/discovery.cc b/usr.sbin/ctld/discovery.cc index 9fca8502c81c..8f6d371b696d 100644 --- a/usr.sbin/ctld/discovery.cc +++ b/usr.sbin/ctld/discovery.cc @@ -113,6 +113,9 @@ discovery_add_target(struct keys *response_keys, const struct target *targ) if (pg == nullptr) continue; for (const portal_up &portal : pg->portals()) { + if (portal->protocol() != portal_protocol::ISCSI && + portal->protocol() != portal_protocol::ISER) + continue; ai = portal->ai(); ret = getnameinfo(ai->ai_addr, ai->ai_addrlen, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), diff --git a/usr.sbin/ctld/kernel.cc b/usr.sbin/ctld/kernel.cc index 00330555a118..b214cd4e8c29 100644 --- a/usr.sbin/ctld/kernel.cc +++ b/usr.sbin/ctld/kernel.cc @@ -126,10 +126,13 @@ struct cctl_port { std::string port_name; int pp; int vp; + uint16_t portid; int cfiscsi_state; std::string cfiscsi_target; + std::string nqn; uint16_t cfiscsi_portal_group_tag; std::string ctld_portal_group_name; + std::string ctld_transport_group_name; attr_list attr_list; }; @@ -322,6 +325,14 @@ cctl_end_pelement(void *user_data, const char *name) cur_port->cfiscsi_portal_group_tag = strtoul(str.c_str(), NULL, 0); } else if (strcmp(name, "ctld_portal_group_name") == 0) { cur_port->ctld_portal_group_name = std::move(str); + } else if (strcmp(name, "ctld_transport_group_name") == 0) { + cur_port->ctld_transport_group_name = std::move(str); + } else if (strcmp(name, "nqn") == 0) { + cur_port->nqn = std::move(str); + } else if (strcmp(name, "portid") == 0) { + if (str.empty()) + log_errx(1, "%s: %s missing its argument", __func__, name); + cur_port->portid = strtoul(str.c_str(), NULL, 0); } else if (strcmp(name, "targ_port") == 0) { devlist->cur_port = NULL; } else if (strcmp(name, "ctlportlist") == 0) { @@ -469,7 +480,7 @@ add_iscsi_port(struct kports &kports, struct conf *conf, if (pg == nullptr) { pg = conf->add_portal_group(pg_name); if (pg == nullptr) { - log_warnx("Failed to add portal_group \"%s\"", pg_name); + log_warnx("Failed to add portal-group \"%s\"", pg_name); return; } } @@ -480,6 +491,43 @@ add_iscsi_port(struct kports &kports, struct conf *conf, } } +void +add_nvmf_port(struct conf *conf, const struct cctl_port &port, + std::string &name) +{ + if (port.nqn.empty() || port.ctld_transport_group_name.empty()) { + log_debugx("CTL port %u \"%s\" wasn't managed by ctld; ", + port.port_id, name.c_str()); + return; + } + + const char *nqn = port.nqn.c_str(); + struct target *targ = conf->find_controller(nqn); + if (targ == nullptr) { + targ = conf->add_controller(nqn); + if (targ == nullptr) { + log_warnx("Failed to add controller \"%s\"", nqn); + return; + } + } + + const char *tg_name = port.ctld_transport_group_name.c_str(); + struct portal_group *pg = conf->find_transport_group(tg_name); + if (pg == nullptr) { + pg = conf->add_transport_group(tg_name); + if (pg == nullptr) { + log_warnx("Failed to add transport-group \"%s\"", + tg_name); + return; + } + } + pg->set_tag(port.portid); + if (!conf->add_port(targ, pg, port.port_id)) { + log_warnx("Failed to add port for controller \"%s\" and transport-group \"%s\"", + nqn, tg_name); + } +} + conf_up conf_new_from_kernel(struct kports &kports) { @@ -505,6 +553,8 @@ conf_new_from_kernel(struct kports &kports) if (port.port_frontend == "iscsi") { add_iscsi_port(kports, conf.get(), port, name); + } else if (port.port_frontend == "nvmf") { + add_nvmf_port(conf.get(), port, name); } else { /* XXX: Treat all unknown ports as iSCSI? */ add_iscsi_port(kports, conf.get(), port, name); @@ -1060,7 +1110,7 @@ void kernel_capsicate(void) { cap_rights_t rights; - const unsigned long cmds[] = { CTL_ISCSI }; + const unsigned long cmds[] = { CTL_ISCSI, CTL_NVMF }; cap_rights_init(&rights, CAP_IOCTL); if (caph_rights_limit(ctl_fd, &rights) < 0) 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(¶ms, 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); +} diff --git a/usr.sbin/ctld/nvmf.hh b/usr.sbin/ctld/nvmf.hh new file mode 100644 index 000000000000..0b4f8d45adfd --- /dev/null +++ b/usr.sbin/ctld/nvmf.hh @@ -0,0 +1,71 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Chelsio Communications, Inc. + * Written by: John Baldwin <jhb@FreeBSD.org> + */ + +#ifndef __NVMF_HH__ +#define __NVMF_HH__ + +struct nvmf_association_deleter { + void operator()(struct nvmf_association *na) const + { + nvmf_free_association(na); + } +}; + +using nvmf_association_up = std::unique_ptr<nvmf_association, + nvmf_association_deleter>; + +struct nvmf_capsule_deleter { + void operator()(struct nvmf_capsule *nc) const + { + nvmf_free_capsule(nc); + } +}; + +using nvmf_capsule_up = std::unique_ptr<nvmf_capsule, nvmf_capsule_deleter>; + +struct nvmf_qpair_deleter { + void operator()(struct nvmf_qpair *qp) const + { + nvmf_free_qpair(qp); + } +}; + +using nvmf_qpair_up = std::unique_ptr<nvmf_qpair, nvmf_qpair_deleter>; + +struct nvmf_portal : public portal { + nvmf_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) : + portal(pg, listen, protocol, std::move(ai)), + p_aparams(aparams), p_association(std::move(na)) {} + virtual ~nvmf_portal() override = default; + + const struct nvmf_association_params *aparams() const + { return &p_aparams; } + +protected: + struct nvmf_association *association() { return p_association.get(); } + +private: + struct nvmf_association_params p_aparams; + nvmf_association_up p_association; +}; + +struct nvmf_discovery_portal final : public nvmf_portal { + nvmf_discovery_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; +}; + +#endif /* !__NVMF_HH__ */ diff --git a/usr.sbin/ctld/nvmf_discovery.cc b/usr.sbin/ctld/nvmf_discovery.cc new file mode 100644 index 000000000000..a32094ddafa1 --- /dev/null +++ b/usr.sbin/ctld/nvmf_discovery.cc @@ -0,0 +1,518 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2023-2025 Chelsio Communications, Inc. + * Written by: John Baldwin <jhb@FreeBSD.org> + */ + +#include <assert.h> +#include <errno.h> +#include <netdb.h> +#include <libiscsiutil.h> +#include <libnvmf.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <netinet/in.h> + +#include "ctld.hh" +#include "nvmf.hh" + +struct discovery_log { + discovery_log(const struct portal_group *pg); + + const char *data() const { return buf.data(); } + size_t length() const { return buf.size(); } + + void append(const struct nvme_discovery_log_entry *entry); + +private: + struct nvme_discovery_log *header() + { return reinterpret_cast<struct nvme_discovery_log *>(buf.data()); } + + std::vector<char> buf; +}; + +struct discovery_controller { + discovery_controller(freebsd::fd_up s, struct nvmf_qpair *qp, + const discovery_log &discovery_log); + + void handle_admin_commands(); +private: + bool update_cc(uint32_t new_cc); + void handle_property_get(const struct nvmf_capsule *nc, + const struct nvmf_fabric_prop_get_cmd *pget); + void handle_property_set(const struct nvmf_capsule *nc, + const struct nvmf_fabric_prop_set_cmd *pset); + void handle_fabrics_command(const struct nvmf_capsule *nc, + const struct nvmf_fabric_cmd *cmd); + void handle_identify_command(const struct nvmf_capsule *nc, + const struct nvme_command *cmd); + void handle_get_log_page_command(const struct nvmf_capsule *nc, + const struct nvme_command *cmd); + + struct nvmf_qpair *qp; + + uint64_t cap = 0; + uint32_t vs = 0; + uint32_t cc = 0; + uint32_t csts = 0; + + bool shutdown = false; + + struct nvme_controller_data cdata; + + const discovery_log &discovery_log; + freebsd::fd_up s; +}; + +discovery_log::discovery_log(const struct portal_group *pg) : + buf(sizeof(nvme_discovery_log)) +{ + struct nvme_discovery_log *log = header(); + + log->genctr = htole32(pg->conf()->genctr()); + log->recfmt = 0; +} + +void +discovery_log::append(const struct nvme_discovery_log_entry *entry) +{ + const char *cp = reinterpret_cast<const char *>(entry); + buf.insert(buf.end(), cp, cp + sizeof(*entry)); + + struct nvme_discovery_log *log = header(); + log->numrec = htole32(le32toh(log->numrec) + 1); +} + +static bool +discovery_controller_filtered(const struct portal_group *pg, + const struct sockaddr *client_sa, std::string_view hostnqn, + const struct port *port) +{ + const struct target *targ = port->target(); + const struct auth_group *ag = port->auth_group(); + if (ag == nullptr) + ag = targ->auth_group(); + + assert(pg->discovery_filter() != discovery_filter::UNKNOWN); + + if (pg->discovery_filter() >= discovery_filter::PORTAL && + !ag->host_permitted(client_sa)) { + log_debugx("host address does not match addresses " + "allowed for controller \"%s\"; skipping", targ->name()); + return true; + } + + if (pg->discovery_filter() >= discovery_filter::PORTAL_NAME && + !ag->host_permitted(hostnqn) != 0) { + log_debugx("HostNQN does not match NQNs " + "allowed for controller \"%s\"; skipping", targ->name()); + return true; + } + + /* XXX: auth not yet implemented for NVMe */ + + return false; +} + +static bool +portal_uses_wildcard_address(const struct portal *p) +{ + const struct addrinfo *ai = p->ai(); + + switch (ai->ai_family) { + case AF_INET: + { + const struct sockaddr_in *sin; + + sin = (const struct sockaddr_in *)ai->ai_addr; + return sin->sin_addr.s_addr == htonl(INADDR_ANY); + } + case AF_INET6: + { + const struct sockaddr_in6 *sin6; + + sin6 = (const struct sockaddr_in6 *)ai->ai_addr; + return memcmp(&sin6->sin6_addr, &in6addr_any, + sizeof(in6addr_any)) == 0; + } + default: + __assert_unreachable(); + } +} + +static bool +init_discovery_log_entry(struct nvme_discovery_log_entry *entry, + const struct target *target, const struct portal *portal, + const char *wildcard_host) +{ + /* + * The TCP port for I/O controllers might not be fixed, so + * fetch the sockaddr of the socket to determine which port + * the kernel chose. + */ + struct sockaddr_storage ss; + socklen_t len = sizeof(ss); + if (getsockname(portal->socket(), (struct sockaddr *)&ss, &len) == -1) { + log_warn("Failed getsockname building discovery log entry"); + return false; + } + + const struct nvmf_association_params *aparams = + static_cast<const nvmf_portal *>(portal)->aparams(); + + memset(entry, 0, sizeof(*entry)); + entry->trtype = NVMF_TRTYPE_TCP; + int error = getnameinfo((struct sockaddr *)&ss, len, + (char *)entry->traddr, sizeof(entry->traddr), + (char *)entry->trsvcid, sizeof(entry->trsvcid), + NI_NUMERICHOST | NI_NUMERICSERV); + if (error != 0) { + log_warnx("Failed getnameinfo building discovery log entry: %s", + gai_strerror(error)); + return false; + } + + if (portal_uses_wildcard_address(portal)) + strncpy((char *)entry->traddr, wildcard_host, + sizeof(entry->traddr)); + switch (portal->ai()->ai_family) { + case AF_INET: + entry->adrfam = NVMF_ADRFAM_IPV4; + break; + case AF_INET6: + entry->adrfam = NVMF_ADRFAM_IPV6; + break; + default: + __assert_unreachable(); + } + entry->subtype = NVMF_SUBTYPE_NVME; + if (!aparams->sq_flow_control) + entry->treq |= (1 << 2); + entry->portid = htole16(portal->portal_group()->tag()); + entry->cntlid = htole16(NVMF_CNTLID_DYNAMIC); + entry->aqsz = aparams->max_admin_qsize; + strncpy((char *)entry->subnqn, target->name(), sizeof(entry->subnqn)); + return true; +} + +static discovery_log +build_discovery_log_page(const struct portal_group *pg, int fd, + const struct sockaddr *client_sa, + const struct nvmf_fabric_connect_data &data) +{ + discovery_log discovery_log(pg); + + struct sockaddr_storage ss; + socklen_t len = sizeof(ss); + if (getsockname(fd, (struct sockaddr *)&ss, &len) == -1) { + log_warn("build_discovery_log_page: getsockname"); + return discovery_log; + } + + char wildcard_host[NI_MAXHOST]; + int error = getnameinfo((struct sockaddr *)&ss, len, wildcard_host, + sizeof(wildcard_host), NULL, 0, NI_NUMERICHOST); + if (error != 0) { + log_warnx("build_discovery_log_page: getnameinfo: %s", + gai_strerror(error)); + return discovery_log; + } + + const char *nqn = (const char *)data.hostnqn; + std::string hostnqn(nqn, strnlen(nqn, sizeof(data.hostnqn))); + for (const auto &kv : pg->ports()) { + const struct port *port = kv.second; + if (discovery_controller_filtered(pg, client_sa, hostnqn, port)) + continue; + + for (const portal_up &portal : pg->portals()) { + if (portal->protocol() != portal_protocol::NVME_TCP) + continue; + + if (portal_uses_wildcard_address(portal.get()) && + portal->ai()->ai_family != client_sa->sa_family) + continue; + + struct nvme_discovery_log_entry entry; + if (init_discovery_log_entry(&entry, port->target(), + portal.get(), wildcard_host)) + discovery_log.append(&entry); + } + } + + return discovery_log; +} + +bool +discovery_controller::update_cc(uint32_t new_cc) +{ + uint32_t changes; + + if (shutdown) + return false; + if (!nvmf_validate_cc(qp, cap, cc, new_cc)) + return false; + + changes = cc ^ new_cc; + cc = new_cc; + + /* Handle shutdown requests. */ + if (NVMEV(NVME_CC_REG_SHN, changes) != 0 && + NVMEV(NVME_CC_REG_SHN, new_cc) != 0) { + csts &= ~NVMEM(NVME_CSTS_REG_SHST); + csts |= NVMEF(NVME_CSTS_REG_SHST, NVME_SHST_COMPLETE); + shutdown = true; + } + + if (NVMEV(NVME_CC_REG_EN, changes) != 0) { + if (NVMEV(NVME_CC_REG_EN, new_cc) == 0) { + /* Controller reset. */ + csts = 0; + shutdown = true; + } else + csts |= NVMEF(NVME_CSTS_REG_RDY, 1); + } + return true; +} + +void +discovery_controller::handle_property_get(const struct nvmf_capsule *nc, + const struct nvmf_fabric_prop_get_cmd *pget) +{ + struct nvmf_fabric_prop_get_rsp rsp; + + nvmf_init_cqe(&rsp, nc, 0); + + switch (le32toh(pget->ofst)) { + case NVMF_PROP_CAP: + if (pget->attrib.size != NVMF_PROP_SIZE_8) + goto error; + rsp.value.u64 = htole64(cap); + break; + case NVMF_PROP_VS: + if (pget->attrib.size != NVMF_PROP_SIZE_4) + goto error; + rsp.value.u32.low = htole32(vs); + break; + case NVMF_PROP_CC: + if (pget->attrib.size != NVMF_PROP_SIZE_4) + goto error; + rsp.value.u32.low = htole32(cc); + break; + case NVMF_PROP_CSTS: + if (pget->attrib.size != NVMF_PROP_SIZE_4) + goto error; + rsp.value.u32.low = htole32(csts); + break; + default: + goto error; + } + + nvmf_send_response(nc, &rsp); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +void +discovery_controller::handle_property_set(const struct nvmf_capsule *nc, + const struct nvmf_fabric_prop_set_cmd *pset) +{ + switch (le32toh(pset->ofst)) { + case NVMF_PROP_CC: + if (pset->attrib.size != NVMF_PROP_SIZE_4) + goto error; + if (!update_cc(le32toh(pset->value.u32.low))) + goto error; + break; + default: + goto error; + } + + nvmf_send_success(nc); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +void +discovery_controller::handle_fabrics_command(const struct nvmf_capsule *nc, + const struct nvmf_fabric_cmd *fc) +{ + switch (fc->fctype) { + case NVMF_FABRIC_COMMAND_PROPERTY_GET: + handle_property_get(nc, + (const struct nvmf_fabric_prop_get_cmd *)fc); + break; + case NVMF_FABRIC_COMMAND_PROPERTY_SET: + handle_property_set(nc, + (const struct nvmf_fabric_prop_set_cmd *)fc); + break; + case NVMF_FABRIC_COMMAND_CONNECT: + log_warnx("CONNECT command on connected queue"); + nvmf_send_generic_error(nc, NVME_SC_COMMAND_SEQUENCE_ERROR); + break; + case NVMF_FABRIC_COMMAND_DISCONNECT: + log_warnx("DISCONNECT command on admin queue"); + nvmf_send_error(nc, NVME_SCT_COMMAND_SPECIFIC, + NVMF_FABRIC_SC_INVALID_QUEUE_TYPE); + break; + default: + log_warnx("Unsupported fabrics command %#x", fc->fctype); + nvmf_send_generic_error(nc, NVME_SC_INVALID_OPCODE); + break; + } +} + +void +discovery_controller::handle_identify_command(const struct nvmf_capsule *nc, + const struct nvme_command *cmd) +{ + uint8_t cns; + + cns = le32toh(cmd->cdw10) & 0xFF; + switch (cns) { + case 1: + break; + default: + log_warnx("Unsupported CNS %#x for IDENTIFY", cns); + goto error; + } + + nvmf_send_controller_data(nc, &cdata, sizeof(cdata)); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +void +discovery_controller::handle_get_log_page_command(const struct nvmf_capsule *nc, + const struct nvme_command *cmd) +{ + uint64_t offset; + uint32_t length; + + switch (nvmf_get_log_page_id(cmd)) { + case NVME_LOG_DISCOVERY: + break; + default: + log_warnx("Unsupported log page %u for discovery controller", + nvmf_get_log_page_id(cmd)); + goto error; + } + + offset = nvmf_get_log_page_offset(cmd); + if (offset >= discovery_log.length()) + goto error; + + length = nvmf_get_log_page_length(cmd); + if (length > discovery_log.length() - offset) + length = discovery_log.length() - offset; + + nvmf_send_controller_data(nc, discovery_log.data() + offset, length); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +void +discovery_controller::handle_admin_commands() +{ + for (;;) { + struct nvmf_capsule *nc; + int error = nvmf_controller_receive_capsule(qp, &nc); + if (error != 0) { + if (error != ECONNRESET) + log_warnc(error, + "Failed to read command capsule"); + break; + } + nvmf_capsule_up nc_guard(nc); + + const struct nvme_command *cmd = + (const struct nvme_command *)nvmf_capsule_sqe(nc); + + /* + * Only permit Fabrics commands while a controller is + * disabled. + */ + if (NVMEV(NVME_CC_REG_EN, cc) == 0 && + cmd->opc != NVME_OPC_FABRICS_COMMANDS) { + log_warnx("Unsupported admin opcode %#x while disabled\n", + cmd->opc); + nvmf_send_generic_error(nc, + NVME_SC_COMMAND_SEQUENCE_ERROR); + continue; + } + + switch (cmd->opc) { + case NVME_OPC_FABRICS_COMMANDS: + handle_fabrics_command(nc, + (const struct nvmf_fabric_cmd *)cmd); + break; + case NVME_OPC_IDENTIFY: + handle_identify_command(nc, cmd); + break; + case NVME_OPC_GET_LOG_PAGE: + handle_get_log_page_command(nc, cmd); + break; + default: + log_warnx("Unsupported admin opcode %#x", cmd->opc); + nvmf_send_generic_error(nc, NVME_SC_INVALID_OPCODE); + break; + } + } +} + +discovery_controller::discovery_controller(freebsd::fd_up fd, + struct nvmf_qpair *qp, const struct discovery_log &discovery_log) : + qp(qp), discovery_log(discovery_log), s(std::move(fd)) +{ + nvmf_init_discovery_controller_data(qp, &cdata); + cap = nvmf_controller_cap(qp); + vs = cdata.ver; +} + +void +nvmf_discovery_portal::handle_connection(freebsd::fd_up fd, + const char *host __unused, const struct sockaddr *client_sa) +{ + 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 discovery qpair: %s", + nvmf_association_error(association())); + return; + } + nvmf_capsule_up nc_guard(nc); + + if (strncmp((char *)data.subnqn, NVMF_DISCOVERY_NQN, + sizeof(data.subnqn)) != 0) { + log_warnx("Discovery NVMe qpair with invalid SubNQN: %.*s", + (int)sizeof(data.subnqn), data.subnqn); + nvmf_connect_invalid_parameters(nc, true, + offsetof(struct nvmf_fabric_connect_data, subnqn)); + return; + } + + /* Just use a controller ID of 1 for all discovery controllers. */ + int error = nvmf_finish_accept(nc, 1); + if (error != 0) { + log_warnc(error, "Failed to send NVMe CONNECT reponse"); + return; + } + nc_guard.reset(); + + discovery_log discovery_log = build_discovery_log_page(portal_group(), + fd, client_sa, data); + + discovery_controller controller(std::move(fd), qp.get(), discovery_log); + controller.handle_admin_commands(); +} diff --git a/usr.sbin/ctld/parse.y b/usr.sbin/ctld/parse.y index 432183ed794c..5725c16b459a 100644 --- a/usr.sbin/ctld/parse.y +++ b/usr.sbin/ctld/parse.y @@ -54,12 +54,14 @@ extern void yyrestart(FILE *); %} %token ALIAS AUTH_GROUP AUTH_TYPE BACKEND BLOCKSIZE CHAP CHAP_MUTUAL -%token CLOSING_BRACKET CTL_LUN DEBUG DEVICE_ID DEVICE_TYPE -%token DISCOVERY_AUTH_GROUP DISCOVERY_FILTER DSCP FOREIGN +%token CLOSING_BRACKET CONTROLLER CTL_LUN DEBUG DEVICE_ID DEVICE_TYPE +%token DISCOVERY_AUTH_GROUP DISCOVERY_FILTER DISCOVERY_TCP DSCP FOREIGN +%token HOST_ADDRESS HOST_NQN %token INITIATOR_NAME INITIATOR_PORTAL ISNS_SERVER ISNS_PERIOD ISNS_TIMEOUT -%token LISTEN LISTEN_ISER LUN MAXPROC OFFLOAD OPENING_BRACKET OPTION +%token LISTEN LISTEN_ISER LUN MAXPROC NAMESPACE +%token OFFLOAD OPENING_BRACKET OPTION %token PATH PCP PIDFILE PORT PORTAL_GROUP REDIRECT SEMICOLON SERIAL -%token SIZE STR TAG TARGET TIMEOUT +%token SIZE STR TAG TARGET TCP TIMEOUT TRANSPORT_GROUP %token AF11 AF12 AF13 AF21 AF22 AF23 AF31 AF32 AF33 AF41 AF42 AF43 %token BE EF CS0 CS1 CS2 CS3 CS4 CS5 CS6 CS7 @@ -98,9 +100,13 @@ statement: | portal_group | + transport_group + | lun | target + | + controller ; debug: DEBUG STR @@ -232,6 +238,10 @@ auth_group_entry: | auth_group_chap_mutual | + auth_group_host_address + | + auth_group_host_nqn + | auth_group_initiator_name | auth_group_initiator_portal @@ -274,6 +284,28 @@ auth_group_chap_mutual: CHAP_MUTUAL STR STR STR STR } ; +auth_group_host_address: HOST_ADDRESS STR + { + bool ok; + + ok = auth_group_add_host_address($2); + free($2); + if (!ok) + return (1); + } + ; + +auth_group_host_nqn: HOST_NQN STR + { + bool ok; + + ok = auth_group_add_host_nqn($2); + free($2); + if (!ok) + return (1); + } + ; + auth_group_initiator_name: INITIATOR_NAME STR { bool ok; @@ -502,6 +534,71 @@ portal_group_pcp: PCP STR } ; +transport_group: TRANSPORT_GROUP transport_group_name + OPENING_BRACKET transport_group_entries CLOSING_BRACKET + { + portal_group_finish(); + } + ; + +transport_group_name: STR + { + bool ok; + + ok = transport_group_start($1); + free($1); + if (!ok) + return (1); + } + ; + +transport_group_entries: + | + transport_group_entries transport_group_entry + | + transport_group_entries transport_group_entry SEMICOLON + ; + +transport_group_entry: + portal_group_discovery_auth_group + | + portal_group_discovery_filter + | + transport_group_listen_discovery_tcp + | + transport_group_listen_tcp + | + portal_group_option + | + portal_group_tag + | + portal_group_dscp + | + portal_group_pcp + ; + +transport_group_listen_discovery_tcp: LISTEN DISCOVERY_TCP STR + { + bool ok; + + ok = transport_group_add_listen_discovery_tcp($3); + free($3); + if (!ok) + return (1); + } + ; + +transport_group_listen_tcp: LISTEN TCP STR + { + bool ok; + + ok = transport_group_add_listen_tcp($3); + free($3); + if (!ok) + return (1); + } + ; + lun: LUN lun_name OPENING_BRACKET lun_entries CLOSING_BRACKET { @@ -738,6 +835,133 @@ target_lun_ref: LUN STR STR } ; +controller: CONTROLLER controller_name + OPENING_BRACKET controller_entries CLOSING_BRACKET + { + target_finish(); + } + ; + +controller_name: STR + { + bool ok; + + ok = controller_start($1); + free($1); + if (!ok) + return (1); + } + ; + +controller_entries: + | + controller_entries controller_entry + | + controller_entries controller_entry SEMICOLON + ; + +controller_entry: + target_auth_group + | + target_auth_type + | + controller_host_address + | + controller_host_nqn + | + controller_transport_group + | + controller_namespace + | + controller_namespace_ref + ; + +controller_host_address: HOST_ADDRESS STR + { + bool ok; + + ok = controller_add_host_address($2); + free($2); + if (!ok) + return (1); + } + ; + +controller_host_nqn: HOST_NQN STR + { + bool ok; + + ok = controller_add_host_nqn($2); + free($2); + if (!ok) + return (1); + } + ; + +controller_transport_group: TRANSPORT_GROUP STR STR + { + bool ok; + + ok = target_add_portal_group($2, $3); + free($2); + free($3); + if (!ok) + return (1); + } + | TRANSPORT_GROUP STR + { + bool ok; + + ok = target_add_portal_group($2, NULL); + free($2); + if (!ok) + return (1); + } + ; + +controller_namespace: NAMESPACE ns_number + OPENING_BRACKET lun_entries CLOSING_BRACKET + { + lun_finish(); + } + ; + +ns_number: STR + { + uint64_t tmp; + + if (expand_number($1, &tmp) != 0) { + yyerror("invalid numeric value"); + free($1); + return (1); + } + free($1); + + if (!controller_start_namespace(tmp)) + return (1); + } + ; + +controller_namespace_ref: NAMESPACE STR STR + { + uint64_t tmp; + bool ok; + + if (expand_number($2, &tmp) != 0) { + yyerror("invalid numeric value"); + free($2); + free($3); + return (1); + } + free($2); + + ok = controller_add_namespace(tmp, $3); + free($3); + if (!ok) + return (1); + } + ; + lun_entries: | lun_entries lun_entry diff --git a/usr.sbin/ctld/token.l b/usr.sbin/ctld/token.l index c8f54103db55..5f959f648969 100644 --- a/usr.sbin/ctld/token.l +++ b/usr.sbin/ctld/token.l @@ -54,21 +54,26 @@ backend { return BACKEND; } blocksize { return BLOCKSIZE; } chap { return CHAP; } chap-mutual { return CHAP_MUTUAL; } +controller { return CONTROLLER; } ctl-lun { return CTL_LUN; } debug { return DEBUG; } device-id { return DEVICE_ID; } device-type { return DEVICE_TYPE; } discovery-auth-group { return DISCOVERY_AUTH_GROUP; } discovery-filter { return DISCOVERY_FILTER; } +discovery-tcp { return DISCOVERY_TCP; } dscp { return DSCP; } pcp { return PCP; } foreign { return FOREIGN; } +host-address { return HOST_ADDRESS; } +host-nqn { return HOST_NQN; } initiator-name { return INITIATOR_NAME; } initiator-portal { return INITIATOR_PORTAL; } listen { return LISTEN; } listen-iser { return LISTEN_ISER; } lun { return LUN; } maxproc { return MAXPROC; } +namespace { return NAMESPACE; } offload { return OFFLOAD; } option { return OPTION; } path { return PATH; } @@ -83,7 +88,9 @@ serial { return SERIAL; } size { return SIZE; } tag { return TAG; } target { return TARGET; } +tcp { return TCP; } timeout { return TIMEOUT; } +transport-group { return TRANSPORT_GROUP; } af11 { return AF11; } af12 { return AF12; } af13 { return AF13; } diff --git a/usr.sbin/ctld/uclparse.cc b/usr.sbin/ctld/uclparse.cc index 843e727a2e52..cb3b0a17cd74 100644 --- a/usr.sbin/ctld/uclparse.cc +++ b/usr.sbin/ctld/uclparse.cc @@ -64,6 +64,10 @@ static bool uclparse_lun(const char *, const ucl::Ucl &); static bool uclparse_lun_entries(const char *, const ucl::Ucl &); static bool uclparse_auth_group(const char *, const ucl::Ucl &); static bool uclparse_portal_group(const char *, const ucl::Ucl &); +static bool uclparse_transport_group(const char *, const ucl::Ucl &); +static bool uclparse_controller(const char *, const ucl::Ucl &); +static bool uclparse_controller_transport_group(const char *, const ucl::Ucl &); +static bool uclparse_controller_namespace(const char *, const ucl::Ucl &); static bool uclparse_target(const char *, const ucl::Ucl &); static bool uclparse_target_portal_group(const char *, const ucl::Ucl &); static bool uclparse_target_lun(const char *, const ucl::Ucl &); @@ -230,6 +234,47 @@ uclparse_target_portal_group(const char *t_name, const ucl::Ucl &obj) } static bool +uclparse_controller_transport_group(const char *t_name, const ucl::Ucl &obj) +{ + /* + * If the value is a single string, assume it is a + * transport-group name. + */ + if (obj.type() == UCL_STRING) + return target_add_portal_group(obj.string_value().c_str(), + nullptr); + + if (obj.type() != UCL_OBJECT) { + log_warnx("transport-group section in controller \"%s\" must " + "be an object or string", t_name); + return false; + } + + auto portal_group = obj["name"]; + if (!portal_group || portal_group.type() != UCL_STRING) { + log_warnx("transport-group section in controller \"%s\" is " + "missing \"name\" string key", t_name); + return false; + } + + auto auth_group = obj["auth-group-name"]; + if (auth_group) { + if (auth_group.type() != UCL_STRING) { + log_warnx("\"auth-group-name\" property in " + "transport-group section for controller \"%s\" is " + "not a string", t_name); + return false; + } + return target_add_portal_group( + portal_group.string_value().c_str(), + auth_group.string_value().c_str()); + } + + return target_add_portal_group(portal_group.string_value().c_str(), + nullptr); +} + +static bool uclparse_target_lun(const char *t_name, const ucl::Ucl &obj) { char *end; @@ -285,6 +330,62 @@ uclparse_target_lun(const char *t_name, const ucl::Ucl &obj) } static bool +uclparse_controller_namespace(const char *t_name, const ucl::Ucl &obj) +{ + char *end; + u_int id; + + std::string key = obj.key(); + if (!key.empty()) { + id = strtoul(key.c_str(), &end, 0); + if (*end != '\0') { + log_warnx("namespace key \"%s\" in controller \"%s\"" + " is invalid", key.c_str(), t_name); + return false; + } + + if (obj.type() == UCL_STRING) + return controller_add_namespace(id, + obj.string_value().c_str()); + } + + if (obj.type() != UCL_OBJECT) { + log_warnx("namespace section entries in controller \"%s\"" + " must be objects", t_name); + return false; + } + + if (key.empty()) { + auto num = obj["number"]; + if (!num || num.type() != UCL_INT) { + log_warnx("namespace section in controller \"%s\" is " + "missing \"id\" integer property", t_name); + return (false); + } + id = num.int_value(); + } + + auto name = obj["name"]; + if (!name) { + if (!controller_start_namespace(id)) + return false; + + std::string lun_name = + freebsd::stringf("namespace %u for controller \"%s\"", id, + t_name); + return uclparse_lun_entries(lun_name.c_str(), obj); + } + + if (name.type() != UCL_STRING) { + log_warnx("\"name\" property for namespace %u for " + "controller \"%s\" is not a string", id, t_name); + return (false); + } + + return controller_add_namespace(id, name.string_value().c_str()); +} + +static bool uclparse_toplevel(const ucl::Ucl &top) { /* Pass 1 - everything except targets */ @@ -390,6 +491,19 @@ uclparse_toplevel(const ucl::Ucl &top) } } + if (key == "transport-group") { + if (obj.type() == UCL_OBJECT) { + for (const auto &child : obj) { + if (!uclparse_transport_group( + child.key().c_str(), child)) + return false; + } + } else { + log_warnx("\"transport-group\" section is not an object"); + return false; + } + } + if (key == "lun") { if (obj.type() == UCL_OBJECT) { for (const auto &child : obj) { @@ -408,6 +522,19 @@ uclparse_toplevel(const ucl::Ucl &top) for (const auto &obj : top) { std::string key = obj.key(); + if (key == "controller") { + if (obj.type() == UCL_OBJECT) { + for (const auto &child : obj) { + if (!uclparse_controller( + child.key().c_str(), child)) + return false; + } + } else { + log_warnx("\"controller\" section is not an object"); + return false; + } + } + if (key == "target") { if (obj.type() == UCL_OBJECT) { for (const auto &child : obj) { @@ -474,6 +601,44 @@ uclparse_auth_group(const char *name, const ucl::Ucl &top) } } + if (key == "host-address") { + if (obj.type() == UCL_STRING) { + if (!auth_group_add_host_address( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!auth_group_add_host_address( + tmp.string_value().c_str())) + return false; + } + } else { + log_warnx("\"host-address\" property of " + "auth-group \"%s\" is not an array or string", + name); + return false; + } + } + + if (key == "host-nqn") { + if (obj.type() == UCL_STRING) { + if (!auth_group_add_host_nqn( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!auth_group_add_host_nqn( + tmp.string_value().c_str())) + return false; + } + } else { + log_warnx("\"host-nqn\" property of " + "auth-group \"%s\" is not an array or string", + name); + return false; + } + } + if (key == "initiator-name") { if (obj.type() == UCL_STRING) { if (!auth_group_add_initiator_name( @@ -739,6 +904,222 @@ uclparse_portal_group(const char *name, const ucl::Ucl &top) } static bool +uclparse_transport_listen_obj(const char *pg_name, const ucl::Ucl &top) +{ + for (const auto &obj : top) { + std::string key = obj.key(); + + if (key.empty()) { + log_warnx("missing protocol for \"listen\" " + "property of transport-group \"%s\"", pg_name); + return false; + } + + if (key == "tcp") { + if (obj.type() == UCL_STRING) { + if (!transport_group_add_listen_tcp( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!transport_group_add_listen_tcp( + tmp.string_value().c_str())) + return false; + } + } + } else if (key == "discovery-tcp") { + if (obj.type() == UCL_STRING) { + if (!transport_group_add_listen_discovery_tcp( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!transport_group_add_listen_discovery_tcp( + tmp.string_value().c_str())) + return false; + } + } + } else { + log_warnx("invalid listen protocol \"%s\" for " + "transport-group \"%s\"", key.c_str(), pg_name); + return false; + } + } + return true; +} + +static bool +uclparse_transport_group(const char *name, const ucl::Ucl &top) +{ + if (!transport_group_start(name)) + return false; + + scope_exit finisher(portal_group_finish); + for (const auto &obj : top) { + std::string key = obj.key(); + + if (key == "discovery-auth-group") { + if (obj.type() != UCL_STRING) { + log_warnx("\"discovery-auth-group\" property " + "of transport-group \"%s\" is not a string", + name); + return false; + } + + if (!portal_group_set_discovery_auth_group( + obj.string_value().c_str())) + return false; + } + + if (key == "discovery-filter") { + if (obj.type() != UCL_STRING) { + log_warnx("\"discovery-filter\" property of " + "transport-group \"%s\" is not a string", + name); + return false; + } + + if (!portal_group_set_filter( + obj.string_value().c_str())) + return false; + } + + if (key == "listen") { + if (obj.type() != UCL_OBJECT) { + log_warnx("\"listen\" property of " + "transport-group \"%s\" is not an object", + name); + return false; + } + if (!uclparse_transport_listen_obj(name, obj)) + return false; + } + + if (key == "options") { + if (obj.type() != UCL_OBJECT) { + log_warnx("\"options\" property of transport group " + "\"%s\" is not an object", name); + return false; + } + + for (const auto &tmp : obj) { + if (!portal_group_add_option( + tmp.key().c_str(), + tmp.forced_string_value().c_str())) + return false; + } + } + + if (key == "dscp") { + if (!uclparse_dscp("transport", name, obj)) + return false; + } + + if (key == "pcp") { + if (!uclparse_pcp("transport", name, obj)) + return false; + } + } + + return true; +} + +static bool +uclparse_controller(const char *name, const ucl::Ucl &top) +{ + if (!controller_start(name)) + return false; + + scope_exit finisher(target_finish); + for (const auto &obj : top) { + std::string key = obj.key(); + + if (key == "auth-group") { + if (obj.type() != UCL_STRING) { + log_warnx("\"auth-group\" property of " + "controller \"%s\" is not a string", name); + return false; + } + + if (!target_set_auth_group(obj.string_value().c_str())) + return false; + } + + if (key == "auth-type") { + if (obj.type() != UCL_STRING) { + log_warnx("\"auth-type\" property of " + "controller \"%s\" is not a string", name); + return false; + } + + if (!target_set_auth_type(obj.string_value().c_str())) + return false; + } + + if (key == "host-address") { + if (obj.type() == UCL_STRING) { + if (!controller_add_host_address( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!controller_add_host_address( + tmp.string_value().c_str())) + return false; + } + } else { + log_warnx("\"host-address\" property of " + "controller \"%s\" is not an array or " + "string", name); + return false; + } + } + + if (key == "host-nqn") { + if (obj.type() == UCL_STRING) { + if (!controller_add_host_nqn( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!controller_add_host_nqn( + tmp.string_value().c_str())) + return false; + } + } else { + log_warnx("\"host-nqn\" property of " + "controller \"%s\" is not an array or " + "string", name); + return false; + } + } + + if (key == "transport-group") { + if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!uclparse_controller_transport_group(name, + tmp)) + return false; + } + } else { + if (!uclparse_controller_transport_group(name, + obj)) + return false; + } + } + + if (key == "namespace") { + for (const auto &tmp : obj) { + if (!uclparse_controller_namespace(name, tmp)) + return false; + } + } + } + + return true; +} + +static bool uclparse_target(const char *name, const ucl::Ucl &top) { if (!target_start(name)) |
