diff --git a/usr.sbin/ctld/Makefile b/usr.sbin/ctld/Makefile --- a/usr.sbin/ctld/Makefile +++ b/usr.sbin/ctld/Makefile @@ -6,17 +6,19 @@ PACKAGE= iscsi PROG= ctld SRCS= ctld.c isns.c kernel.c +SRCS+= nvme.c nvme_discovery.c SRCS+= parse.y token.l y.tab.h uclparse.c 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/libnvmf #CFLAGS+= -DICL_KERNEL_PROXY NO_WCAST_ALIGN= MAN= ctld.8 ctl.conf.5 -LIBADD= bsdxml iscsiutil md sbuf util ucl m nv +LIBADD= bsdxml iscsiutil nvmf md sbuf util ucl m nv YFLAGS+= -v CLEANFILES= y.tab.c y.tab.h y.output diff --git a/usr.sbin/ctld/ctl.conf.5 b/usr.sbin/ctld/ctl.conf.5 --- a/usr.sbin/ctld/ctl.conf.5 +++ b/usr.sbin/ctld/ctl.conf.5 @@ -31,7 +31,7 @@ .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 @@ .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 @@ .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 @@ 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 @@ 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,58 @@ .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 +The CTL-specific port options passed to the kernel. +.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 +482,101 @@ 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 an ephimeral TCP port 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 +597,7 @@ 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 +612,11 @@ 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 +685,16 @@ 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 @@ -562,6 +759,14 @@ vendor = "FreeBSD" } } + + example_3 { + backend = ramdisk + size = 1G + options { + capacity = 1G + } + } } target { @@ -589,6 +794,17 @@ ] } } + +controller { + "nqn.2012-06.com.example:controller1" { + auth-group = no-authentication + namespace = [ + { nsid = 1, name = example_1 }, + { nsid = 2, name = example_3 } + ] + } +} + .Ed .Sh SEE ALSO .Xr ctl 4 , diff --git a/usr.sbin/ctld/ctld.h b/usr.sbin/ctld/ctld.h --- a/usr.sbin/ctld/ctld.h +++ b/usr.sbin/ctld/ctld.h @@ -49,6 +49,8 @@ #define MAX_LUNS 1024 struct ctl_req; +struct nvmf_association; +struct nvmf_association_params; struct port; struct target_protocol_ops; @@ -94,10 +96,14 @@ TAILQ_HEAD(, auth) ag_auths; struct auth_name_head ag_initiator_names; struct auth_portal_head ag_initiator_portals; + struct auth_name_head ag_host_names; + struct auth_portal_head ag_host_addresses; }; #define PORTAL_PROTOCOL_ISCSI 0 #define PORTAL_PROTOCOL_ISER 1 +#define PORTAL_PROTOCOL_NVME_TCP 2 +#define PORTAL_PROTOCOL_NVME_DISCOVERY_TCP 3 struct portal { TAILQ_ENTRY(portal) p_next; @@ -111,9 +117,17 @@ TAILQ_HEAD(, target) p_targets; int p_socket; + + union { + struct { + struct nvmf_association_params *aparams; + struct nvmf_association *association; + } p_nvme; + }; }; #define TARGET_PROTOCOL_ISCSI 0 +#define TARGET_PROTOCOL_NVME 1 struct target_protocol_ops { /* Initialize protocol-specific state for a new portal group. */ @@ -249,6 +263,7 @@ int conf_debug; int conf_timeout; int conf_maxproc; + uint32_t conf_genctr; #ifdef ICL_KERNEL_PROXY int conf_portal_id; @@ -256,6 +271,7 @@ struct pidfh *conf_pidfh; bool conf_default_pg_defined; + bool conf_default_tg_defined; bool conf_default_ag_defined; bool conf_kernel_port_on; }; @@ -291,6 +307,7 @@ extern bool proxy_mode; extern int ctl_fd; extern struct target_protocol_ops target_iscsi; +extern struct target_protocol_ops target_nvme; int parse_conf(struct conf *newconf, const char *path); int uclparse_conf(struct conf *conf, const char *path); @@ -349,6 +366,9 @@ int portal_group_set_redirection(struct portal_group *pg, const char *addr); +int transport_group_set_filter(struct portal_group *pg, + const char *filter); + int isns_new(struct conf *conf, const char *addr); void isns_delete(struct isns *is); void isns_register(struct isns *isns, struct isns *oldisns); @@ -426,4 +446,7 @@ void set_timeout(int timeout, int fatal); +void nvme_handle_discovery_socket(struct portal *p, int s, + const struct sockaddr *client_sa); + #endif /* !CTLD_H */ diff --git a/usr.sbin/ctld/ctld.c b/usr.sbin/ctld/ctld.c --- a/usr.sbin/ctld/ctld.c +++ b/usr.sbin/ctld/ctld.c @@ -74,6 +74,7 @@ struct conf * conf_new(void) { + static uint32_t genctr; struct conf *conf; conf = calloc(1, sizeof(*conf)); @@ -91,6 +92,7 @@ conf->conf_debug = 0; conf->conf_timeout = 60; conf->conf_maxproc = 30; + conf->conf_genctr = genctr++; return (conf); } @@ -285,6 +287,8 @@ switch (protocol) { case TARGET_PROTOCOL_ISCSI: return (&ag->ag_initiator_names); + case TARGET_PROTOCOL_NVME: + return (&ag->ag_host_names); default: __assert_unreachable(); } @@ -296,6 +300,8 @@ switch (protocol) { case TARGET_PROTOCOL_ISCSI: return (&ag->ag_initiator_names); + case TARGET_PROTOCOL_NVME: + return (&ag->ag_host_names); default: __assert_unreachable(); } @@ -371,6 +377,8 @@ switch (protocol) { case TARGET_PROTOCOL_ISCSI: return (&ag->ag_initiator_portals); + case TARGET_PROTOCOL_NVME: + return (&ag->ag_host_addresses); default: __assert_unreachable(); } @@ -382,6 +390,8 @@ switch (protocol) { case TARGET_PROTOCOL_ISCSI: return (&ag->ag_initiator_portals); + case TARGET_PROTOCOL_NVME: + return (&ag->ag_host_addresses); default: __assert_unreachable(); } @@ -547,6 +557,8 @@ TAILQ_INIT(&ag->ag_auths); TAILQ_INIT(&ag->ag_initiator_names); TAILQ_INIT(&ag->ag_initiator_portals); + TAILQ_INIT(&ag->ag_host_names); + TAILQ_INIT(&ag->ag_host_addresses); ag->ag_conf = conf; TAILQ_INSERT_TAIL(&conf->conf_auth_groups, ag, ag_next); @@ -570,6 +582,12 @@ TAILQ_FOREACH_SAFE(auth_portal, &ag->ag_initiator_portals, ap_next, auth_portal_tmp) auth_portal_delete(auth_portal); + TAILQ_FOREACH_SAFE(auth_name, &ag->ag_host_names, an_next, + auth_name_tmp) + auth_name_delete(auth_name); + TAILQ_FOREACH_SAFE(auth_portal, &ag->ag_host_addresses, ap_next, + auth_portal_tmp) + auth_portal_delete(auth_portal); free(ag->ag_name); free(ag); } @@ -662,6 +680,8 @@ case TARGET_PROTOCOL_ISCSI: return (&target_iscsi); #endif + case TARGET_PROTOCOL_NVME: + return (&target_nvme); default: return (NULL); } @@ -673,6 +693,8 @@ switch (protocol) { case TARGET_PROTOCOL_ISCSI: return "portal-group"; + case TARGET_PROTOCOL_NVME: + return "transport-group"; default: __assert_unreachable(); } @@ -814,6 +836,12 @@ case PORTAL_PROTOCOL_ISCSI: def_port = "3260"; break; + case PORTAL_PROTOCOL_NVME_TCP: + def_port = "4420"; + break; + case PORTAL_PROTOCOL_NVME_DISCOVERY_TCP: + def_port = "8009"; + break; default: __builtin_unreachable(); } @@ -1131,6 +1159,38 @@ return (0); } +int +transport_group_set_filter(struct portal_group *pg, const char *str) +{ + int filter; + + if (strcmp(str, "none") == 0) { + filter = PG_FILTER_NONE; + } else if (strcmp(str, "address") == 0) { + filter = PG_FILTER_PORTAL; + } else if (strcmp(str, "address-name") == 0) { + filter = PG_FILTER_PORTAL_NAME; + } else { + log_warnx("invalid discovery-filter \"%s\" for transport-group " + "\"%s\"; valid values are \"none\", \"address\", " + "and \"address-name\"", + str, pg->pg_name); + return (1); + } + + if (pg->pg_discovery_filter != PG_FILTER_UNKNOWN && + pg->pg_discovery_filter != filter) { + log_warnx("cannot set discovery-filter to \"%s\" for " + "transport-group \"%s\"; already has a different " + "value", str, pg->pg_name); + return (1); + } + + pg->pg_discovery_filter = filter; + + return (0); +} + int portal_group_set_offload(struct portal_group *pg, const char *offload) { @@ -1626,6 +1686,12 @@ TAILQ_FOREACH(auth_portal, &ag->ag_initiator_portals, ap_next) fprintf(stderr, "\t initiator-portal %s\n", auth_portal->ap_portal); + TAILQ_FOREACH(auth_name, &ag->ag_host_names, an_next) + fprintf(stderr, "\t host-nqn %s\n", + auth_name->an_name); + TAILQ_FOREACH(auth_portal, &ag->ag_host_addresses, ap_next) + fprintf(stderr, "\t host-address %s\n", + auth_portal->ap_portal); fprintf(stderr, "}\n"); } TAILQ_FOREACH(pg, &conf->conf_portal_groups, pg_next) { @@ -2591,6 +2657,9 @@ pg = portal_group_new(conf, TARGET_PROTOCOL_ISCSI, "default"); assert(pg != NULL); + pg = portal_group_new(conf, TARGET_PROTOCOL_NVME, "default"); + assert(pg != NULL); + if (ucl) error = uclparse_conf(conf, path); else @@ -2620,6 +2689,19 @@ portal_group_add_listen(pg, "[::]", PORTAL_PROTOCOL_ISCSI); } + if (conf->conf_default_tg_defined == false) { + log_debugx("transport-group \"default\" not defined; " + "going with defaults"); + pg = portal_group_find(conf, TARGET_PROTOCOL_NVME, "default"); + assert(pg != NULL); + portal_group_add_listen(pg, "0.0.0.0", + PORTAL_PROTOCOL_NVME_DISCOVERY_TCP); + portal_group_add_listen(pg, "[::]", + PORTAL_PROTOCOL_NVME_DISCOVERY_TCP); + portal_group_add_listen(pg, "0.0.0.0", PORTAL_PROTOCOL_NVME_TCP); + portal_group_add_listen(pg, "[::]", PORTAL_PROTOCOL_NVME_TCP); + } + conf->conf_kernel_port_on = true; error = conf_verify(conf); diff --git a/usr.sbin/ctld/kernel.c b/usr.sbin/ctld/kernel.c --- a/usr.sbin/ctld/kernel.c +++ b/usr.sbin/ctld/kernel.c @@ -117,8 +117,10 @@ char *port_name; int pp; int vp; + uint16_t portid; int cfiscsi_state; char *cfiscsi_target; + char *nqn; uint16_t cfiscsi_portal_group_tag; char *ctld_portal_group_name; nvlist_t *attr_list; @@ -358,6 +360,13 @@ } else if (strcmp(name, "ctld_portal_group_name") == 0) { cur_port->ctld_portal_group_name = str; str = NULL; + } else if (strcmp(name, "nqn") == 0) { + cur_port->nqn = str; + str = NULL; + } else if (strcmp(name, "portid") == 0) { + if (str == NULL) + log_errx(1, "%s: %s missing its argument", __func__, name); + cur_port->portid = strtoul(str, NULL, 0); } else if (strcmp(name, "targ_port") == 0) { devlist->cur_port = NULL; } else if (strcmp(name, "ctlportlist") == 0) { @@ -515,6 +524,8 @@ continue; if (strcmp(port->port_frontend, "iscsi") == 0) protocol = TARGET_PROTOCOL_ISCSI; + else if (strcmp(port->port_frontend, "nvmf") == 0) + protocol = TARGET_PROTOCOL_NVME; else /* XXX: Treat all unknown ports as iSCSI? */ protocol = TARGET_PROTOCOL_ISCSI; @@ -537,6 +548,9 @@ case TARGET_PROTOCOL_ISCSI: target_name = port->cfiscsi_target; break; + case TARGET_PROTOCOL_NVME: + target_name = port->nqn; + break; default: __assert_unreachable(); break; @@ -598,6 +612,9 @@ case TARGET_PROTOCOL_ISCSI: pg->pg_tag = port->cfiscsi_portal_group_tag; break; + case TARGET_PROTOCOL_NVME: + pg->pg_tag = port->portid; + break; default: __assert_unreachable(); break; @@ -614,6 +631,7 @@ free(port->port_frontend); free(port->port_name); free(port->cfiscsi_target); + free(port->nqn); free(port->ctld_portal_group_name); nvlist_destroy(port->attr_list); free(port); @@ -1188,7 +1206,7 @@ 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/nvme.c b/usr.sbin/ctld/nvme.c new file mode 100644 --- /dev/null +++ b/usr.sbin/ctld/nvme.c @@ -0,0 +1,339 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Chelsio Communications, Inc. + * Written by: John Baldwin + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ctld.h" + +#define DEFAULT_MAXH2CDATA (256 * 1024) + +static uint16_t nvme_last_port_id = 0; + +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; + uint64_t uval; + + if (!nvlist_exists_string(nvl, key)) + return (def); + + value = nvlist_get_string(nvl, key); + if (expand_number(value, &uval) == 0 && uval >= minv && uval <= maxv) + return (uval); + + log_warnx("Invalid value \"%s\" for numeric option %s", value, key); + return (def); +} + +/* Options shared between discovery and I/O associations. */ +static void +nvme_aparams_from_options(struct portal_group *pg, + struct nvmf_association_params *params) +{ + uint64_t value; + + params->tcp.header_digests = parse_bool(pg->pg_options, "HDGST", false); + params->tcp.data_digests = parse_bool(pg->pg_options, "DDGST", false); + value = parse_number(pg->pg_options, "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; +} + +static struct nvmf_association_params * +nvme_init_discovery_aparams(struct portal_group *pg) +{ + struct nvmf_association_params *params; + + params = calloc(1, sizeof(*params)); + params->sq_flow_control = false; + params->dynamic_controller_model = true; + params->max_admin_qsize = NVME_MAX_ADMIN_ENTRIES; + params->tcp.pda = 0; + nvme_aparams_from_options(pg, params); + + return (params); +} + +static struct nvmf_association_params * +nvme_init_io_aparams(struct portal_group *pg) +{ + struct nvmf_association_params *params; + + params = calloc(1, sizeof(*params)); + params->sq_flow_control = parse_bool(pg->pg_options, "SQFC", false); + params->dynamic_controller_model = true; + params->max_admin_qsize = parse_number(pg->pg_options, + "max_admin_qsize", NVME_MAX_ADMIN_ENTRIES, NVME_MIN_ADMIN_ENTRIES, + NVME_MAX_ADMIN_ENTRIES); + params->max_io_qsize = parse_number(pg->pg_options, "max_io_qsize", + NVME_MAX_IO_ENTRIES, NVME_MIN_IO_ENTRIES, NVME_MAX_IO_ENTRIES); + params->tcp.pda = 0; + nvme_aparams_from_options(pg, params); + + return (params); +} + +static void +nvme_portal_group_init(struct portal_group *pg) +{ + pg->pg_tag = ++nvme_last_port_id; +} + +static void +nvme_portal_group_copy(struct portal_group *oldpg, struct portal_group *newpg) +{ + newpg->pg_tag = oldpg->pg_tag; +} + +static void +nvme_portal_init(struct portal *p) +{ + struct portal_group *pg = p->p_portal_group; + struct nvmf_association_params *aparams; + enum nvmf_trtype trtype; + + switch (p->p_protocol) { + case PORTAL_PROTOCOL_NVME_TCP: + trtype = NVMF_TRTYPE_TCP; + aparams = nvme_init_io_aparams(pg); + break; + case PORTAL_PROTOCOL_NVME_DISCOVERY_TCP: + trtype = NVMF_TRTYPE_TCP; + aparams = nvme_init_discovery_aparams(pg); + break; + default: + __assert_unreachable(); + } + + p->p_nvme.aparams = aparams; + p->p_nvme.association = nvmf_allocate_association(trtype, true, + aparams); + if (p->p_nvme.association == NULL) + log_err(1, "Failed to create NVMe controller association"); +} + +static void +nvme_portal_init_socket(struct portal *p __unused) +{ +} + +static void +nvme_portal_delete(struct portal *p) +{ + if (p->p_nvme.association != NULL) + nvmf_free_association(p->p_nvme.association); + free(p->p_nvme.aparams); +} + +static void +nvme_load_kernel_modules(struct portal_group *pg) +{ + struct portal *p; + static bool loaded; + bool tcp_transport; + int saved_errno; + + if (loaded) + return; + + saved_errno = errno; + if (modfind("nvmft") == -1 && kldload("nvmft") == -1) + log_warn("couldn't load nvmft"); + + tcp_transport = false; + TAILQ_FOREACH(p, &pg->pg_portals, p_next) { + switch (p->p_protocol) { + case PORTAL_PROTOCOL_NVME_TCP: + tcp_transport = true; + break; + } + } + if (tcp_transport) { + if (modfind("nvmf/tcp") == -1 && kldload("nvmf_tcp") == -1) + log_warn("couldn't load nvmf_tcp"); + } + + errno = saved_errno; + loaded = true; +} + +static void +nvme_kernel_port_add(struct port *port, struct ctl_req *req) +{ + struct target *targ = port->p_target; + struct portal_group *pg = port->p_portal_group; + + nvme_load_kernel_modules(pg); + + strlcpy(req->driver, "nvmf", sizeof(req->driver)); + + nvlist_add_string(req->args_nvl, "subnqn", targ->t_name); + nvlist_add_stringf(req->args_nvl, "portid", "%u", pg->pg_tag); + if (!nvlist_exists_string(req->args_nvl, "max_io_qsize")) + nvlist_add_stringf(req->args_nvl, "max_io_qsize", "%u", + NVME_MAX_IO_ENTRIES); +} + +static void +nvme_kernel_port_remove(struct port *port, struct ctl_req *req) +{ + struct target *targ = port->p_target; + + strlcpy(req->driver, "nvmf", sizeof(req->driver)); + + nvlist_add_string(req->args_nvl, "subnqn", targ->t_name); +} + +static char * +nvme_validate_target_name(const char *name) +{ + char *t_name; + size_t i, len; + + if (!nvmf_nqn_valid_strict(name)) { + log_warnx("controller name \"%s\" is invalid for NVMe", name); + return (NULL); + } + + t_name = strdup(name); + if (t_name == NULL) { + log_warn("strdup"); + return (NULL); + } + + /* + * Normalize the name to lowercase to match iSCSI. + */ + len = strlen(t_name); + for (i = 0; i < len; i++) + t_name[i] = tolower(t_name[i]); + + return (t_name); +} + +static void +nvme_handle_io_socket(struct portal *portal, int fd) +{ + struct nvmf_fabric_connect_data data; + struct nvmf_qpair_params qparams; + struct ctl_nvmf req; + const struct nvmf_fabric_connect_cmd *cmd; + struct nvmf_capsule *nc; + struct nvmf_qpair *qp; + int error; + + memset(&qparams, 0, sizeof(qparams)); + qparams.tcp.fd = fd; + + nc = NULL; + qp = nvmf_accept(portal->p_nvme.association, &qparams, &nc, &data); + if (qp == NULL) { + log_warnx("Failed to create NVMe I/O qpair: %s", + nvmf_association_error(portal->p_nvme.association)); + goto error; + } + cmd = nvmf_capsule_sqe(nc); + + memset(&req, 0, sizeof(req)); + req.type = CTL_NVMF_HANDOFF; + error = nvmf_handoff_controller_qpair(qp, cmd, &data, + &req.data.handoff); + if (error != 0) { + log_warnc(error, + "Failed to prepare NVMe I/O qpair for handoff"); + goto error; + } + + 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); + +error: + if (nc != NULL) + nvmf_free_capsule(nc); + if (qp != NULL) + nvmf_free_qpair(qp); + close(fd); +} + +static void +nvme_handle_connection(struct portal *portal, int fd, const char *host __unused, + const struct sockaddr *client_sa) +{ + switch (portal->p_protocol) { + case PORTAL_PROTOCOL_NVME_TCP: + nvme_handle_io_socket(portal, fd); + break; + case PORTAL_PROTOCOL_NVME_DISCOVERY_TCP: + nvme_handle_discovery_socket(portal, fd, client_sa); + break; + default: + __assert_unreachable(); + } +} + +struct target_protocol_ops target_nvme = { + .portal_group_init = nvme_portal_group_init, + .portal_group_copy = nvme_portal_group_copy, + .portal_init = nvme_portal_init, + .portal_init_socket = nvme_portal_init_socket, + .portal_delete = nvme_portal_delete, + .kernel_port_add = nvme_kernel_port_add, + .kernel_port_remove = nvme_kernel_port_remove, + .validate_target_name = nvme_validate_target_name, + .handle_connection = nvme_handle_connection, +}; diff --git a/usr.sbin/ctld/nvme_discovery.c b/usr.sbin/ctld/nvme_discovery.c new file mode 100644 --- /dev/null +++ b/usr.sbin/ctld/nvme_discovery.c @@ -0,0 +1,532 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2023-2025 Chelsio Communications, Inc. + * Written by: John Baldwin + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ctld.h" + +struct io_controller_data { + struct nvme_discovery_log_entry entry; + bool wildcard; +}; + +struct controller { + struct nvmf_qpair *qp; + + uint64_t cap; + uint32_t vs; + uint32_t cc; + uint32_t csts; + + bool shutdown; + + struct nvme_controller_data cdata; + + struct portal *portal; + const struct sockaddr *client_sa; + char *hostnqn; + struct nvme_discovery_log *discovery_log; + size_t discovery_log_len; + int s; +}; + +static bool +discovery_controller_filtered(struct controller *c, + const struct port *port) +{ + const struct portal_group *pg = c->portal->p_portal_group; + const struct target *targ = port->p_target; + const struct auth_group *ag; + + ag = port->p_auth_group; + if (ag == NULL) + ag = targ->t_auth_group; + + assert(pg->pg_discovery_auth_group != PG_FILTER_UNKNOWN); + + if (pg->pg_discovery_filter >= PG_FILTER_PORTAL && + auth_portal_check(ag, TARGET_PROTOCOL_NVME, + (const struct sockaddr_storage *)c->client_sa) != 0) { + log_debugx("host address does not match addresses " + "allowed for controller \"%s\"; skipping", targ->t_name); + return (true); + } + + + if (pg->pg_discovery_filter >= PG_FILTER_PORTAL_NAME && + auth_name_check(ag, TARGET_PROTOCOL_NVME, c->hostnqn) != 0) { + log_debugx("HostNQN does not match NQNs " + "allowed for controller \"%s\"; skipping", targ->t_name); + return (true); + } + + /* XXX: auth not yet implemented for NVMe */ + + return (false); +} + +static bool +portal_uses_wildcard_address(const struct portal *p) +{ + struct addrinfo *ai = p->p_ai; + + switch (ai->ai_family) { + case AF_INET: + { + struct sockaddr_in *sin; + + sin = (struct sockaddr_in *)ai->ai_addr; + return (sin->sin_addr.s_addr == htonl(INADDR_ANY)); + } + case AF_INET6: + { + struct sockaddr_in6 *sin6; + + sin6 = (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, + struct target *target, struct portal *portal, const char *wildcard_host) +{ + const struct nvmf_association_params *aparams = portal->p_nvme.aparams; + struct portal_group *pg = portal->p_portal_group; + struct sockaddr_storage ss; + struct addrinfo *ai = portal->p_ai; + int error; + socklen_t len; + + /* + * The default TCP port for I/O controllers is zero, so fetch + * the sockaddr of the socket to determine which port the + * kernel chose. + */ + len = sizeof(ss); + if (getsockname(portal->p_socket, (struct sockaddr *)&ss, &len) == -1) { + log_warn("Failed getsockname building discovery log entry"); + return (false); + } + + memset(entry, 0, sizeof(*entry)); + entry->trtype = NVMF_TRTYPE_TCP; + error = getnameinfo((struct sockaddr *)&ss, len, entry->traddr, + sizeof(entry->traddr), 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(entry->traddr, wildcard_host, sizeof(entry->traddr)); + switch (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(pg->pg_tag); + entry->cntlid = htole16(NVMF_CNTLID_DYNAMIC); + entry->aqsz = aparams->max_admin_qsize; + strncpy(entry->subnqn, target->t_name, sizeof(entry->subnqn)); + return (true); +} + +static void +build_discovery_log_page(struct controller *c) +{ + struct portal_group *pg = c->portal->p_portal_group; + struct portal *portal; + struct port *port; + struct sockaddr_storage ss; + socklen_t len; + char wildcard_host[NI_MAXHOST]; + u_int nentries; + int error; + + if (c->discovery_log != NULL) + return; + + len = sizeof(ss); + if (getsockname(c->s, (struct sockaddr *)&ss, &len) == -1) { + log_warn("build_discovery_log_page: getsockname"); + return; + } + + 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; + } + + nentries = 0; + TAILQ_FOREACH(port, &pg->pg_ports, p_pgs) { + if (discovery_controller_filtered(c, port)) + continue; + + TAILQ_FOREACH(portal, &pg->pg_portals, p_next) { + if (portal->p_protocol == + PORTAL_PROTOCOL_NVME_DISCOVERY_TCP) + continue; + + if (portal_uses_wildcard_address(portal) && + portal->p_ai->ai_family != ss.ss_family) + continue; + + nentries++; + } + } + + c->discovery_log_len = sizeof(*c->discovery_log) + + nentries * sizeof(struct nvme_discovery_log_entry); + c->discovery_log = calloc(c->discovery_log_len, 1); + c->discovery_log->genctr = htole32(pg->pg_conf->conf_genctr); + c->discovery_log->recfmt = 0; + nentries = 0; + TAILQ_FOREACH(port, &pg->pg_ports, p_pgs) { + if (discovery_controller_filtered(c, port)) + continue; + + TAILQ_FOREACH(portal, &pg->pg_portals, p_next) { + if (portal->p_protocol == + PORTAL_PROTOCOL_NVME_DISCOVERY_TCP) + continue; + + if (portal_uses_wildcard_address(portal) && + portal->p_ai->ai_family != ss.ss_family) + continue; + + if (init_discovery_log_entry( + &c->discovery_log->entries[nentries], + port->p_target, portal, wildcard_host)) + nentries++; + } + } + c->discovery_log->numrec = nentries; +} + +static bool +update_cc(struct controller *c, uint32_t new_cc) +{ + uint32_t changes; + + if (c->shutdown) + return (false); + if (!nvmf_validate_cc(c->qp, c->cap, c->cc, new_cc)) + return (false); + + changes = c->cc ^ new_cc; + c->cc = new_cc; + + /* Handle shutdown requests. */ + if (NVMEV(NVME_CC_REG_SHN, changes) != 0 && + NVMEV(NVME_CC_REG_SHN, new_cc) != 0) { + c->csts &= ~NVMEM(NVME_CSTS_REG_SHST); + c->csts |= NVMEF(NVME_CSTS_REG_SHST, NVME_SHST_COMPLETE); + c->shutdown = true; + } + + if (NVMEV(NVME_CC_REG_EN, changes) != 0) { + if (NVMEV(NVME_CC_REG_EN, new_cc) == 0) { + /* Controller reset. */ + c->csts = 0; + c->shutdown = true; + } else + c->csts |= NVMEF(NVME_CSTS_REG_RDY, 1); + } + return (true); +} + +static void +handle_property_get(const struct controller *c, 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(c->cap); + break; + case NVMF_PROP_VS: + if (pget->attrib.size != NVMF_PROP_SIZE_4) + goto error; + rsp.value.u32.low = htole32(c->vs); + break; + case NVMF_PROP_CC: + if (pget->attrib.size != NVMF_PROP_SIZE_4) + goto error; + rsp.value.u32.low = htole32(c->cc); + break; + case NVMF_PROP_CSTS: + if (pget->attrib.size != NVMF_PROP_SIZE_4) + goto error; + rsp.value.u32.low = htole32(c->csts); + break; + default: + goto error; + } + + nvmf_send_response(nc, &rsp); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +static void +handle_property_set(struct controller *c, 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(c, 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); +} + +static void +handle_fabrics_command(struct controller *c, const struct nvmf_capsule *nc, + const struct nvmf_fabric_cmd *fc) +{ + switch (fc->fctype) { + case NVMF_FABRIC_COMMAND_PROPERTY_GET: + handle_property_get(c, nc, + (const struct nvmf_fabric_prop_get_cmd *)fc); + break; + case NVMF_FABRIC_COMMAND_PROPERTY_SET: + handle_property_set(c, 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; + } +} + +static void +handle_identify_command(const struct controller *c, + 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, &c->cdata, sizeof(c->cdata)); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +static void +handle_get_log_page_command(struct controller *c, + 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; + } + + build_discovery_log_page(c); + + offset = nvmf_get_log_page_offset(cmd); + if (offset >= c->discovery_log_len) + goto error; + + length = nvmf_get_log_page_length(cmd); + if (length > c->discovery_log_len - offset) + length = c->discovery_log_len - offset; + + nvmf_send_controller_data(nc, (char *)c->discovery_log + offset, + length); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +static void +controller_handle_admin_commands(struct controller *c) +{ + struct nvmf_qpair *qp = c->qp; + const struct nvme_command *cmd; + struct nvmf_capsule *nc; + int error; + + for (;;) { + error = nvmf_controller_receive_capsule(qp, &nc); + if (error != 0) { + if (error != ECONNRESET) + log_warnc(error, + "Failed to read command capsule"); + break; + } + + cmd = nvmf_capsule_sqe(nc); + + /* + * Only permit Fabrics commands while a controller is + * disabled. + */ + if (NVMEV(NVME_CC_REG_EN, c->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); + nvmf_free_capsule(nc); + continue; + } + + switch (cmd->opc) { + case NVME_OPC_FABRICS_COMMANDS: + handle_fabrics_command(c, nc, + (const struct nvmf_fabric_cmd *)cmd); + break; + case NVME_OPC_IDENTIFY: + handle_identify_command(c, nc, cmd); + break; + case NVME_OPC_GET_LOG_PAGE: + handle_get_log_page_command(c, nc, cmd); + break; + default: + log_warnx("Unsupported admin opcode %#x", cmd->opc); + nvmf_send_generic_error(nc, NVME_SC_INVALID_OPCODE); + break; + } + nvmf_free_capsule(nc); + } +} + +static void +nvme_discovery(struct portal *p, int s, const struct sockaddr *client_sa, + struct nvmf_qpair *qp, const struct nvmf_fabric_connect_data *data) +{ + struct controller c; + + memset(&c, 0, sizeof(c)); + c.portal = p; + c.client_sa = client_sa; + c.hostnqn = strndup(data->hostnqn, sizeof(data->hostnqn)); + c.s = s; + c.qp = qp; + nvmf_init_discovery_controller_data(qp, &c.cdata); + c.cap = nvmf_controller_cap(qp); + c.vs = c.cdata.ver; + + controller_handle_admin_commands(&c); + + free(c.discovery_log); + free(c.hostnqn); +} + +void +nvme_handle_discovery_socket(struct portal *portal, int s, + const struct sockaddr *client_sa) +{ + struct nvmf_fabric_connect_data data; + struct nvmf_qpair_params qparams; + struct nvmf_capsule *nc; + struct nvmf_qpair *qp; + int error; + + memset(&qparams, 0, sizeof(qparams)); + qparams.tcp.fd = s; + + nc = NULL; + qp = nvmf_accept(portal->p_nvme.association, &qparams, &nc, &data); + if (qp == NULL) { + log_warnx("Failed to create NVMe discovery qpair: %s", + nvmf_association_error(portal->p_nvme.association)); + goto error; + } + + if (strcmp(data.subnqn, NVMF_DISCOVERY_NQN) != 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)); + goto error; + } + + /* Just use a controller ID of 1 for all discovery controllers. */ + error = nvmf_finish_accept(nc, 1); + if (error != 0) { + log_warnc(error, "Failed to send NVMe CONNECT reponse"); + goto error; + } + nvmf_free_capsule(nc); + nc = NULL; + + nvme_discovery(portal, s, client_sa, qp, &data); +error: + if (nc != NULL) + nvmf_free_capsule(nc); + if (qp != NULL) + nvmf_free_qpair(qp); + close(s); +} + diff --git a/usr.sbin/ctld/parse.y b/usr.sbin/ctld/parse.y --- a/usr.sbin/ctld/parse.y +++ b/usr.sbin/ctld/parse.y @@ -58,12 +58,14 @@ %} %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 @@ -102,9 +104,13 @@ | portal_group | + transport_group + | lun | target + | + controller ; debug: DEBUG STR @@ -239,6 +245,10 @@ | auth_group_chap_mutual | + auth_group_host_address + | + auth_group_host_nqn + | auth_group_initiator_name | auth_group_initiator_portal @@ -281,6 +291,28 @@ } ; +auth_group_host_address: HOST_ADDRESS STR + { + const struct auth_portal *ap; + + ap = auth_portal_new(auth_group, TARGET_PROTOCOL_NVME, $2); + free($2); + if (ap == NULL) + return (1); + } + ; + +auth_group_host_nqn: HOST_NQN STR + { + const struct auth_name *an; + + an = auth_name_new(auth_group, TARGET_PROTOCOL_NVME, $2); + free($2); + if (an == NULL) + return (1); + } + ; + auth_group_initiator_name: INITIATOR_NAME STR { const struct auth_name *an; @@ -534,6 +566,114 @@ } ; +transport_group: TRANSPORT_GROUP transport_group_name + OPENING_BRACKET transport_group_entries CLOSING_BRACKET + { + portal_group = NULL; + } + ; + +transport_group_name: STR + { + /* + * Make it possible to redefine default + * transport-group. but only once. + */ + if (strcmp($1, "default") == 0 && + conf->conf_default_tg_defined == false) { + portal_group = portal_group_find(conf, + TARGET_PROTOCOL_NVME, $1); + conf->conf_default_tg_defined = true; + } else { + portal_group = portal_group_new(conf, + TARGET_PROTOCOL_NVME, $1); + } + free($1); + if (portal_group == NULL) + return (1); + } + ; + +transport_group_entries: + | + transport_group_entries transport_group_entry + | + transport_group_entries transport_group_entry SEMICOLON + ; + +transport_group_entry: + transport_group_discovery_auth_group + | + transport_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_discovery_auth_group: DISCOVERY_AUTH_GROUP STR + { + if (portal_group->pg_discovery_auth_group != NULL) { + log_warnx("discovery-auth-group for transport-group " + "\"%s\" specified more than once", + portal_group->pg_name); + return (1); + } + portal_group->pg_discovery_auth_group = + auth_group_find(conf, $2); + if (portal_group->pg_discovery_auth_group == NULL) { + log_warnx("unknown discovery-auth-group \"%s\" " + "for transport-group \"%s\"", + $2, portal_group->pg_name); + return (1); + } + free($2); + } + ; + +transport_group_discovery_filter: DISCOVERY_FILTER STR + { + int error; + + error = transport_group_set_filter(portal_group, $2); + free($2); + if (error != 0) + return (1); + } + ; + +transport_group_listen_discovery_tcp: LISTEN DISCOVERY_TCP STR + { + int error; + + error = portal_group_add_listen(portal_group, $3, + PORTAL_PROTOCOL_NVME_DISCOVERY_TCP); + free($3); + if (error != 0) + return (1); + } + ; + +transport_group_listen_tcp: LISTEN TCP STR + { + int error; + + error = portal_group_add_listen(portal_group, $3, + PORTAL_PROTOCOL_NVME_TCP); + free($3); + if (error != 0) + return (1); + } + ; + lun: LUN lun_name OPENING_BRACKET lun_entries CLOSING_BRACKET { @@ -918,6 +1058,229 @@ } ; +controller: CONTROLLER controller_name + OPENING_BRACKET controller_entries CLOSING_BRACKET + { + target = NULL; + } + ; + +controller_name: STR + { + target = target_new(conf, $1, TARGET_PROTOCOL_NVME); + free($1); + if (target == NULL) + 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 + { + const struct auth_portal *ap; + + if (target->t_auth_group != NULL) { + if (target->t_auth_group->ag_name != NULL) { + log_warnx("cannot use both auth-group and " + "host-address for controller \"%s\"", + target->t_name); + free($2); + return (1); + } + } else { + target->t_auth_group = auth_group_new(conf, NULL); + if (target->t_auth_group == NULL) { + free($2); + return (1); + } + target->t_auth_group->ag_target = target; + } + ap = auth_portal_new(target->t_auth_group, + TARGET_PROTOCOL_NVME, $2); + free($2); + if (ap == NULL) + return (1); + } + ; + +controller_host_nqn: HOST_NQN STR + { + const struct auth_name *an; + + if (target->t_auth_group != NULL) { + if (target->t_auth_group->ag_name != NULL) { + log_warnx("cannot use both auth-group and " + "host-nqn for controller \"%s\"", + target->t_name); + free($2); + return (1); + } + } else { + target->t_auth_group = auth_group_new(conf, NULL); + if (target->t_auth_group == NULL) { + free($2); + return (1); + } + target->t_auth_group->ag_target = target; + } + an = auth_name_new(target->t_auth_group, TARGET_PROTOCOL_NVME, + $2); + free($2); + if (an == NULL) + return (1); + } + ; + +controller_transport_group: TRANSPORT_GROUP STR STR + { + struct portal_group *tpg; + struct auth_group *tag; + struct port *tp; + + tpg = portal_group_find(conf, TARGET_PROTOCOL_NVME, $2); + if (tpg == NULL) { + log_warnx("unknown transport-group \"%s\" for controller " + "\"%s\"", $2, target->t_name); + free($2); + free($3); + return (1); + } + tag = auth_group_find(conf, $3); + if (tag == NULL) { + log_warnx("unknown auth-group \"%s\" for controller " + "\"%s\"", $3, target->t_name); + free($2); + free($3); + return (1); + } + tp = port_new(conf, target, tpg); + if (tp == NULL) { + log_warnx("can't link transport-group \"%s\" to controller " + "\"%s\"", $2, target->t_name); + free($2); + return (1); + } + tp->p_auth_group = tag; + free($2); + free($3); + } + | TRANSPORT_GROUP STR + { + struct portal_group *tpg; + struct port *tp; + + tpg = portal_group_find(conf, TARGET_PROTOCOL_NVME, $2); + if (tpg == NULL) { + log_warnx("unknown transport-group \"%s\" for controller " + "\"%s\"", $2, target->t_name); + free($2); + return (1); + } + tp = port_new(conf, target, tpg); + if (tp == NULL) { + log_warnx("can't link transport-group \"%s\" to controller " + "\"%s\"", $2, target->t_name); + free($2); + return (1); + } + free($2); + } + ; + +controller_namespace: NAMESPACE ns_number + OPENING_BRACKET lun_entries CLOSING_BRACKET + { + lun = NULL; + } + ; + +ns_number: STR + { + uint64_t tmp; + int ret; + char *name; + + if (expand_number($1, &tmp) != 0) { + yyerror("invalid numeric value"); + free($1); + return (1); + } + if (tmp == 0) { + yyerror("namespace ID cannot be 0"); + free($1); + return (1); + } + if (tmp - 1 >= MAX_LUNS) { + yyerror("namespace ID is too big"); + free($1); + return (1); + } + + ret = asprintf(&name, "%s,nsid,%ju", target->t_name, tmp); + if (ret <= 0) + log_err(1, "asprintf"); + lun = lun_new(conf, name); + if (lun == NULL) + return (1); + + lun_set_scsiname(lun, name); + target->t_luns[tmp - 1] = lun; + } + ; + +controller_namespace_ref: NAMESPACE STR STR + { + uint64_t tmp; + + if (expand_number($2, &tmp) != 0) { + yyerror("invalid numeric value"); + free($2); + free($3); + return (1); + } + free($2); + if (tmp == 0) { + yyerror("namespace ID cannot be 0"); + free($3); + return (1); + } + if (tmp - 1 >= MAX_LUNS) { + yyerror("namespace ID is too big"); + free($3); + return (1); + } + + lun = lun_find(conf, $3); + free($3); + if (lun == NULL) + return (1); + + target->t_luns[tmp - 1] = lun; + } + ; + lun_entries: | lun_entries lun_entry diff --git a/usr.sbin/ctld/token.l b/usr.sbin/ctld/token.l --- a/usr.sbin/ctld/token.l +++ b/usr.sbin/ctld/token.l @@ -54,21 +54,26 @@ 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 @@ 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.c b/usr.sbin/ctld/uclparse.c --- a/usr.sbin/ctld/uclparse.c +++ b/usr.sbin/ctld/uclparse.c @@ -51,6 +51,12 @@ static bool uclparse_lun(const char *, const ucl_object_t *); static bool uclparse_auth_group(const char *, const ucl_object_t *); static bool uclparse_portal_group(const char *, const ucl_object_t *); +static bool uclparse_transport_group(const char *, const ucl_object_t *); +static bool uclparse_controller(const char *, const ucl_object_t *); +static bool uclparse_controller_transport_group(struct target *, + const ucl_object_t *); +static bool uclparse_controller_namespace(struct target *, + const ucl_object_t *); static bool uclparse_target(const char *, const ucl_object_t *); static bool uclparse_target_portal_group(struct target *, const ucl_object_t *); static bool uclparse_target_lun(struct target *, const ucl_object_t *); @@ -185,6 +191,58 @@ return (true); } +static bool +uclparse_controller_transport_group(struct target *target, + const ucl_object_t *obj) +{ + struct portal_group *tpg; + struct auth_group *tag = NULL; + struct port *tp; + const ucl_object_t *portal_group, *auth_group; + + portal_group = ucl_object_find_key(obj, "name"); + if (!portal_group || portal_group->type != UCL_STRING) { + log_warnx("transport-group section in controller \"%s\" is " + "missing \"name\" string key", target->t_name); + return (false); + } + + auth_group = ucl_object_find_key(obj, "auth-group-name"); + if (auth_group && auth_group->type != UCL_STRING) { + log_warnx("transport-group section in controller \"%s\" is " + "missing \"auth-group-name\" string key", target->t_name); + return (false); + } + + tpg = portal_group_find(conf, TARGET_PROTOCOL_NVME, + ucl_object_tostring(portal_group)); + if (tpg == NULL) { + log_warnx("unknown transport-group \"%s\" for controller " + "\"%s\"", ucl_object_tostring(portal_group), target->t_name); + return (false); + } + + if (auth_group) { + tag = auth_group_find(conf, ucl_object_tostring(auth_group)); + if (tag == NULL) { + log_warnx("unknown auth-group \"%s\" for controller " + "\"%s\"", ucl_object_tostring(auth_group), + target->t_name); + return (false); + } + } + + tp = port_new(conf, target, tpg); + if (tp == NULL) { + log_warnx("can't link transport-group \"%s\" to controller " + "\"%s\"", ucl_object_tostring(portal_group), target->t_name); + return (false); + } + tp->p_auth_group = tag; + + return (true); +} + static bool uclparse_target_lun(struct target *target, const ucl_object_t *obj) { @@ -243,6 +301,73 @@ return (true); } +static bool +uclparse_controller_namespace(struct target *target, const ucl_object_t *obj) +{ + struct lun *lun; + uint64_t tmp; + + if (obj->type == UCL_INT) { + char *name; + + tmp = ucl_object_toint(obj); + if (tmp == 0) { + log_warnx("namespace ID cannot be 0"); + return (false); + } + if (tmp - 1 >= MAX_LUNS) { + log_warnx("namespace ID %ju in controller \"%s\" is " + "too big", tmp, target->t_name); + return (false); + } + + asprintf(&name, "%s,nsid,%ju", target->t_name, tmp); + lun = lun_new(conf, name); + if (lun == NULL) + return (false); + + lun_set_scsiname(lun, name); + target->t_luns[tmp - 1] = lun; + return (true); + } + + if (obj->type == UCL_OBJECT) { + const ucl_object_t *num = ucl_object_find_key(obj, "nsid"); + const ucl_object_t *name = ucl_object_find_key(obj, "name"); + + if (num == NULL || num->type != UCL_INT) { + log_warnx("namespace section in controller \"%s\" is " + "missing \"nsid\" integer property", + target->t_name); + return (false); + } + tmp = ucl_object_toint(num); + if (tmp == 0) { + log_warnx("namespace ID cannot be 0"); + return (false); + } + if (tmp - 1 >= MAX_LUNS) { + log_warnx("namespace ID %ju in controller \"%s\" is " + "too big", tmp, target->t_name); + return (false); + } + + if (name == NULL || name->type != UCL_STRING) { + log_warnx("namespace section in controller \"%s\" is " + "missing \"name\" string property", target->t_name); + return (false); + } + + lun = lun_find(conf, ucl_object_tostring(name)); + if (lun == NULL) + return (false); + + target->t_luns[tmp - 1] = lun; + } + + return (true); +} + static bool uclparse_toplevel(const ucl_object_t *top) { @@ -354,6 +479,18 @@ } } + if (!strcmp(key, "transport-group")) { + if (obj->type == UCL_OBJECT) { + iter = NULL; + while ((child = ucl_iterate_object(obj, &iter, true))) { + uclparse_transport_group(ucl_object_key(child), child); + } + } else { + log_warnx("\"transport-group\" section is not an object"); + return (false); + } + } + if (!strcmp(key, "lun")) { if (obj->type == UCL_OBJECT) { iter = NULL; @@ -372,6 +509,20 @@ while ((obj = ucl_iterate_object(top, &it, true))) { const char *key = ucl_object_key(obj); + if (!strcmp(key, "controller")) { + if (obj->type == UCL_OBJECT) { + iter = NULL; + while ((child = ucl_iterate_object(obj, &iter, + true))) { + uclparse_controller(ucl_object_key(child), + child); + } + } else { + log_warnx("\"controller\" section is not an object"); + return (false); + } + } + if (!strcmp(key, "target")) { if (obj->type == UCL_OBJECT) { iter = NULL; @@ -453,6 +604,44 @@ } } + if (!strcmp(key, "host-address")) { + if (obj->type != UCL_ARRAY) { + log_warnx("\"host-address\" property of " + "auth-group \"%s\" is not an array", + name); + return (false); + } + + it2 = NULL; + while ((tmp = ucl_iterate_object(obj, &it2, true))) { + const char *value = ucl_object_tostring(tmp); + + ap = auth_portal_new(auth_group, + TARGET_PROTOCOL_NVME, value); + if (ap == NULL) + return (false); + } + } + + if (!strcmp(key, "host-nqn")) { + if (obj->type != UCL_ARRAY) { + log_warnx("\"host-nqn\" property of " + "auth-group \"%s\" is not an array", + name); + return (false); + } + + it2 = NULL; + while ((tmp = ucl_iterate_object(obj, &it2, true))) { + const char *value = ucl_object_tostring(tmp); + + an = auth_name_new(auth_group, + TARGET_PROTOCOL_NVME, value); + if (an == NULL) + return (false); + } + } + if (!strcmp(key, "initiator-name")) { if (obj->type != UCL_ARRAY) { log_warnx("\"initiator-name\" property of " @@ -719,6 +908,296 @@ return (true); } +static int +parse_transport_protocol(const char *name) +{ + if (strcmp(name, "tcp") == 0) + return (PORTAL_PROTOCOL_NVME_TCP); + else if (strcmp(name, "discovery-tcp") == 0) + return (PORTAL_PROTOCOL_NVME_DISCOVERY_TCP); + else + return (-1); +} + +static bool +uclparse_transport_listen_obj(struct portal_group *portal_group, + const ucl_object_t *top) +{ + ucl_object_iter_t it = NULL; + const ucl_object_t *obj = NULL; + const char *key; + int protocol; + + while ((obj = ucl_iterate_object(top, &it, true))) { + key = ucl_object_key(obj); + + protocol = parse_transport_protocol(key); + if (protocol < 0) { + log_warnx("invalid listen protocol \"%s\" for " + "transport-group \"%s\"", key, + portal_group->pg_name); + return (false); + } + + if (portal_group_add_listen(portal_group, + ucl_object_tostring(obj), protocol) != 0) + return (false); + } + return (true); +} + +static bool +uclparse_transport_group(const char *name, const ucl_object_t *top) +{ + struct portal_group *portal_group; + ucl_object_iter_t it = NULL, it2 = NULL; + const ucl_object_t *obj = NULL, *tmp = NULL; + const char *key; + + if (strcmp(name, "default") == 0 && + conf->conf_default_tg_defined == false) { + portal_group = portal_group_find(conf, TARGET_PROTOCOL_NVME, + name); + conf->conf_default_tg_defined = true; + } else { + portal_group = portal_group_new(conf, TARGET_PROTOCOL_NVME, + name); + } + + if (portal_group == NULL) + return (false); + + while ((obj = ucl_iterate_object(top, &it, true))) { + key = ucl_object_key(obj); + + if (!strcmp(key, "discovery-auth-group")) { + portal_group->pg_discovery_auth_group = + auth_group_find(conf, ucl_object_tostring(obj)); + if (portal_group->pg_discovery_auth_group == NULL) { + log_warnx("unknown discovery-auth-group \"%s\" " + "for transport-group \"%s\"", + ucl_object_tostring(obj), + portal_group->pg_name); + return (false); + } + } + + if (!strcmp(key, "discovery-filter")) { + if (obj->type != UCL_STRING) { + log_warnx("\"discovery-filter\" property of " + "portal-group \"%s\" is not a string", + portal_group->pg_name); + return (false); + } + + if (transport_group_set_filter(portal_group, + ucl_object_tostring(obj)) != 0) + return (false); + } + + if (!strcmp(key, "listen")) { + if (obj->type != UCL_OBJECT) { + log_warnx("missing protocol for \"listen\" " + "property of transport-group \"%s\"", + portal_group->pg_name); + return (false); + } + if (!uclparse_transport_listen_obj(portal_group, obj)) + return (false); + } + + if (!strcmp(key, "options")) { + if (obj->type != UCL_OBJECT) { + log_warnx("\"options\" property of transport group " + "\"%s\" is not an object", portal_group->pg_name); + return (false); + } + + while ((tmp = ucl_iterate_object(obj, &it2, + true))) { + nvlist_add_string(portal_group->pg_options, + ucl_object_key(tmp), + ucl_object_tostring_forced(tmp)); + } + } + + if (!strcmp(key, "dscp")) { + if (!uclparse_dscp("transport", portal_group, obj)) + return (false); + } + + if (!strcmp(key, "pcp")) { + if (!uclparse_pcp("transport", portal_group, obj)) + return (false); + } + } + + return (true); +} + +static bool +uclparse_controller(const char *name, const ucl_object_t *top) +{ + struct target *target; + ucl_object_iter_t it = NULL, it2 = NULL; + const ucl_object_t *obj = NULL, *tmp = NULL; + const char *key; + + target = target_new(conf, name, TARGET_PROTOCOL_NVME); + if (target == NULL) + return (false); + + while ((obj = ucl_iterate_object(top, &it, true))) { + key = ucl_object_key(obj); + + if (!strcmp(key, "auth-group")) { + const char *ag; + + if (target->t_auth_group != NULL) { + if (target->t_auth_group->ag_name != NULL) + log_warnx("auth-group for controller " + "\"%s\" specified more than once", + target->t_name); + else + log_warnx("cannot use both auth-group " + "and explicit authorisations for " + "controller \"%s\"", target->t_name); + return (false); + } + ag = ucl_object_tostring(obj); + if (!ag) { + log_warnx("auth-group must be a string"); + return (false); + } + target->t_auth_group = auth_group_find(conf, ag); + if (target->t_auth_group == NULL) { + log_warnx("unknown auth-group \"%s\" for " + "controller \"%s\"", + ucl_object_tostring(obj), target->t_name); + return (false); + } + } + + if (!strcmp(key, "auth-type")) { + int error; + + if (target->t_auth_group != NULL) { + if (target->t_auth_group->ag_name != NULL) { + log_warnx("cannot use both auth-group and " + "auth-type for controller \"%s\"", + target->t_name); + return (false); + } + } else { + target->t_auth_group = auth_group_new(conf, NULL); + if (target->t_auth_group == NULL) + return (false); + + target->t_auth_group->ag_target = target; + } + error = auth_group_set_type(target->t_auth_group, + ucl_object_tostring(obj)); + if (error != 0) + return (false); + } + + if (!strcmp(key, "chap")) { + if (target->t_auth_group != NULL) { + if (target->t_auth_group->ag_name != NULL) { + log_warnx("cannot use both auth-group " + "and chap for controller \"%s\"", + target->t_name); + return (false); + } + } else { + target->t_auth_group = auth_group_new(conf, NULL); + if (target->t_auth_group == NULL) { + return (false); + } + target->t_auth_group->ag_target = target; + } + if (!uclparse_chap(target->t_auth_group, obj)) + return (false); + } + + if (!strcmp(key, "chap-mutual")) { + if (!uclparse_chap_mutual(target->t_auth_group, obj)) + return (false); + } + + if (!strcmp(key, "host-nqn")) { + const struct auth_name *an; + + if (target->t_auth_group != NULL) { + if (target->t_auth_group->ag_name != NULL) { + log_warnx("cannot use both auth-group and " + "host-nqn for controller \"%s\"", + target->t_name); + return (false); + } + } else { + target->t_auth_group = auth_group_new(conf, NULL); + if (target->t_auth_group == NULL) + return (false); + + target->t_auth_group->ag_target = target; + } + an = auth_name_new(target->t_auth_group, + TARGET_PROTOCOL_NVME, ucl_object_tostring(obj)); + if (an == NULL) + return (false); + } + + if (!strcmp(key, "host-address")) { + const struct auth_portal *ap; + + if (target->t_auth_group != NULL) { + if (target->t_auth_group->ag_name != NULL) { + log_warnx("cannot use both auth-group and " + "host-address for controller \"%s\"", + target->t_name); + return (false); + } + } else { + target->t_auth_group = auth_group_new(conf, NULL); + if (target->t_auth_group == NULL) + return (false); + + target->t_auth_group->ag_target = target; + } + ap = auth_portal_new(target->t_auth_group, + TARGET_PROTOCOL_NVME, ucl_object_tostring(obj)); + if (ap == NULL) + return (false); + } + + if (!strcmp(key, "transport-group")) { + if (obj->type == UCL_OBJECT) { + if (!uclparse_controller_transport_group(target, obj)) + return (false); + } + + if (obj->type == UCL_ARRAY) { + while ((tmp = ucl_iterate_object(obj, &it2, + true))) { + if (!uclparse_controller_transport_group(target, + tmp)) + return (false); + } + } + } + + if (!strcmp(key, "namespace")) { + while ((tmp = ucl_iterate_object(obj, &it2, true))) { + if (!uclparse_controller_namespace(target, tmp)) + return (false); + } + } + } + + return (true); +} + static bool uclparse_target(const char *name, const ucl_object_t *top) {