diff options
Diffstat (limited to 'usr.sbin/ctld')
-rw-r--r-- | usr.sbin/ctld/Makefile | 12 | ||||
-rw-r--r-- | usr.sbin/ctld/conf.cc | 72 | ||||
-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 | 531 | ||||
-rw-r--r-- | usr.sbin/ctld/ctld.hh | 174 | ||||
-rw-r--r-- | usr.sbin/ctld/discovery.cc | 8 | ||||
-rw-r--r-- | usr.sbin/ctld/iscsi.cc | 508 | ||||
-rw-r--r-- | usr.sbin/ctld/iscsi.hh | 79 | ||||
-rw-r--r-- | usr.sbin/ctld/kernel.cc | 251 | ||||
-rw-r--r-- | usr.sbin/ctld/login.cc | 17 | ||||
-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 | 383 |
17 files changed, 3077 insertions, 518 deletions
diff --git a/usr.sbin/ctld/Makefile b/usr.sbin/ctld/Makefile index 26cc03c036cb..61efe8a05cfb 100644 --- a/usr.sbin/ctld/Makefile +++ b/usr.sbin/ctld/Makefile @@ -5,30 +5,28 @@ CFLAGS+=-I${SRCTOP}/contrib/libucl/include PACKAGE= ctl PROG_CXX= ctld -SRCS= ctld.cc conf.cc discovery.cc isns.cc kernel.cc -SRCS+= login.cc parse.y token.l y.tab.h uclparse.cc +SRCS= ctld.cc conf.cc discovery.cc iscsi.cc isns.cc kernel.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 NO_WMISSING_VARIABLE_DECLARATIONS= -.if ${MK_ISCSI} != "no" -CFLAGS+= -DWANT_ISCSI -.endif - .include <bsd.prog.mk> CXXWARNFLAGS.uclparse.cc= -Wno-shadow -Wno-cast-qual diff --git a/usr.sbin/ctld/conf.cc b/usr.sbin/ctld/conf.cc index 2eae12c31d0c..56a149a58a25 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)); @@ -175,7 +187,8 @@ portal_group_finish(void) bool portal_group_add_listen(const char *listen, bool iser) { - return (portal_group->add_portal(listen, iser)); + return (portal_group->add_portal(listen, iser ? portal_protocol::ISER : + portal_protocol::ISCSI)); } bool @@ -233,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); @@ -387,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")); @@ -398,7 +466,7 @@ parse_conf(const char *path) bool parsed; try { parsed = yyparse_conf(fp.get()); - } catch (std::bad_alloc) { + } catch (std::bad_alloc &) { log_warnx("failed to allocate memory parsing %s", path); return (false); } catch (...) { 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 23d01364a6a4..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> @@ -58,13 +59,6 @@ #include "ctld.hh" #include "isns.hh" -static bool timed_out(void); -#ifdef ICL_KERNEL_PROXY -static void pdu_receive_proxy(struct pdu *pdu); -static void pdu_send_proxy(struct pdu *pdu); -#endif /* ICL_KERNEL_PROXY */ -static void pdu_fail(const struct connection *conn, const char *reason); - bool proxy_mode = false; static volatile bool sighup_received = false; @@ -73,19 +67,8 @@ static volatile bool sigalrm_received = false; static int kqfd; static int nchildren = 0; -static uint16_t last_portal_group_tag = 0xff; -static struct connection_ops conn_ops = { - .timed_out = timed_out, -#ifdef ICL_KERNEL_PROXY - .pdu_receive_proxy = pdu_receive_proxy, - .pdu_send_proxy = pdu_send_proxy, -#else - .pdu_receive_proxy = nullptr, - .pdu_send_proxy = nullptr, -#endif - .fail = pdu_fail, -}; +uint32_t conf::global_genctr; static void usage(void) @@ -96,6 +79,11 @@ usage(void) exit(1); } +conf::conf() +{ + conf_genctr = global_genctr++; +} + void conf::set_debug(int debug) { @@ -298,20 +286,37 @@ 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. */ - ag_names.emplace(name); + ag_initiator_names.emplace(name); return (true); } bool auth_group::initiator_permitted(std::string_view initiator_name) const { - if (ag_names.empty()) + if (ag_initiator_names.empty()) return (true); - return (ag_names.count(std::string(initiator_name)) != 0); + return (ag_initiator_names.count(std::string(initiator_name)) != 0); } bool @@ -381,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; @@ -390,7 +409,7 @@ auth_group::add_initiator_portal(const char *portal) return (false); } - ag_portals.emplace_back(ap); + ag_initiator_portals.emplace_back(ap); return (true); } @@ -427,12 +446,24 @@ 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_portals.empty()) + if (ag_initiator_portals.empty()) return (true); - for (const auth_portal &ap : ag_portals) + for (const auth_portal &ap : ag_initiator_portals) if (ap.matches(sa)) return (true); return (false); @@ -477,8 +508,8 @@ conf::find_auth_group(std::string_view name) return (it->second); } -portal_group::portal_group(struct conf *conf, std::string_view name) - : pg_conf(conf), pg_options(nvlist_create(0)), pg_name(name) +portal_group::portal_group(struct conf *conf, std::string_view name) : + pg_conf(conf), pg_options(nvlist_create(0)), pg_name(name) { } @@ -486,7 +517,7 @@ struct portal_group * conf::add_portal_group(const char *name) { auto pair = conf_portal_groups.try_emplace(name, - std::make_unique<portal_group>(this, name)); + iscsi_make_portal_group(this, name)); if (!pair.second) { log_warnx("duplicated portal-group \"%s\"", name); return (nullptr); @@ -521,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 { @@ -531,7 +601,7 @@ portal_group::is_dummy() const return (false); } -static freebsd::addrinfo_up +freebsd::addrinfo_up parse_addr_port(const char *address, const char *def_port) { struct addrinfo hints, *ai; @@ -599,25 +669,6 @@ portal_group::options() const } bool -portal_group::add_portal(const char *value, bool iser) -{ - freebsd::addrinfo_up ai = parse_addr_port(value, "3260"); - if (!ai) { - log_warnx("invalid listen address %s", value); - return (false); - } - - /* - * XXX: getaddrinfo(3) may return multiple addresses; we should turn - * those into multiple portals. - */ - - pg_portals.emplace_back(std::make_unique<portal>(this, value, iser, - std::move(ai))); - return (true); -} - -bool portal_group::add_option(const char *name, const char *value) { return (option_new(pg_options.get(), name, value)); @@ -627,14 +678,14 @@ bool portal_group::set_discovery_auth_group(const char *ag_name) { if (pg_discovery_auth_group != nullptr) { - log_warnx("discovery-auth-group for portal-group " - "\"%s\" specified more than once", name()); + log_warnx("discovery-auth-group for %s " + "\"%s\" specified more than once", keyword(), name()); return (false); } pg_discovery_auth_group = pg_conf->find_auth_group(ag_name); if (pg_discovery_auth_group == nullptr) { log_warnx("unknown discovery-auth-group \"%s\" " - "for portal-group \"%s\"", ag_name, name()); + "for %s \"%s\"", ag_name, keyword(), name()); return (false); } return (true); @@ -644,8 +695,8 @@ bool portal_group::set_dscp(u_int dscp) { if (dscp >= 0x40) { - log_warnx("invalid DSCP value %u for portal-group \"%s\"", - dscp, name()); + log_warnx("invalid DSCP value %u for %s \"%s\"", + dscp, keyword(), name()); return (false); } @@ -653,39 +704,6 @@ portal_group::set_dscp(u_int dscp) return (true); } -bool -portal_group::set_filter(const char *str) -{ - enum discovery_filter filter; - - if (strcmp(str, "none") == 0) { - filter = discovery_filter::NONE; - } else if (strcmp(str, "portal") == 0) { - filter = discovery_filter::PORTAL; - } else if (strcmp(str, "portal-name") == 0) { - filter = discovery_filter::PORTAL_NAME; - } else if (strcmp(str, "portal-name-auth") == 0) { - filter = discovery_filter::PORTAL_NAME_AUTH; - } else { - log_warnx("invalid discovery-filter \"%s\" for portal-group " - "\"%s\"; valid values are \"none\", \"portal\", " - "\"portal-name\", and \"portal-name-auth\"", - str, name()); - return (false); - } - - if (pg_discovery_filter != discovery_filter::UNKNOWN && - pg_discovery_filter != filter) { - log_warnx("cannot set discovery-filter to \"%s\" for " - "portal-group \"%s\"; already has a different " - "value", str, name()); - return (false); - } - - pg_discovery_filter = filter; - return (true); -} - void portal_group::set_foreign() { @@ -697,8 +715,8 @@ portal_group::set_offload(const char *offload) { if (!pg_offload.empty()) { log_warnx("cannot set offload to \"%s\" for " - "portal-group \"%s\"; already defined", - offload, name()); + "%s \"%s\"; already defined", + offload, keyword(), name()); return (false); } @@ -710,8 +728,8 @@ bool portal_group::set_pcp(u_int pcp) { if (pcp > 7) { - log_warnx("invalid PCP value %u for portal-group \"%s\"", - pcp, name()); + log_warnx("invalid PCP value %u for %s \"%s\"", + pcp, keyword(), name()); return (false); } @@ -724,8 +742,8 @@ portal_group::set_redirection(const char *addr) { if (!pg_redirection.empty()) { log_warnx("cannot set redirection to \"%s\" for " - "portal-group \"%s\"; already defined", - addr, name()); + "%s \"%s\"; already defined", + addr, keyword(), name()); return (false); } @@ -752,16 +770,17 @@ portal_group::verify(struct conf *conf) if (!pg_redirection.empty()) { if (!pg_ports.empty()) { - log_debugx("portal-group \"%s\" assigned to target, " - "but configured for redirection", name()); + log_debugx("%s \"%s\" assigned to target, " + "but configured for redirection", keyword(), + name()); } pg_assigned = true; } else if (!pg_ports.empty()) { pg_assigned = true; } else { if (pg_name != "default") - log_warnx("portal-group \"%s\" not assigned " - "to any target", name()); + log_warnx("%s \"%s\" not assigned " + "to any target", keyword(), name()); pg_assigned = false; } } @@ -789,8 +808,8 @@ portal_group::open_sockets(struct conf &oldconf) return (0); if (!pg_assigned) { - log_debugx("not listening on portal-group \"%s\", " - "not assigned to any target", name()); + log_debugx("not listening on %s \"%s\", " + "not assigned to any target", keyword(), name()); return (0); } @@ -818,8 +837,8 @@ portal_group::close_sockets() for (portal_up &portal : pg_portals) { if (portal->socket() < 0) continue; - log_debugx("closing socket for %s, portal-group \"%s\"", - portal->listen(), name()); + log_debugx("closing socket for %s, %s \"%s\"", + portal->listen(), keyword(), name()); portal->close(); } } @@ -1111,8 +1130,8 @@ conf::add_port(struct target *target, struct portal_group *pg, auth_group_sp ag) { std::string name = freebsd::stringf("%s-%s", pg->name(), target->name()); - const auto &pair = conf_ports.try_emplace(name, - std::make_unique<portal_group_port>(target, pg, ag)); + const auto &pair = conf_ports.try_emplace(name, pg->create_port(target, + ag)); if (!pair.second) { log_warnx("duplicate port \"%s\"", name.c_str()); return (false); @@ -1127,8 +1146,8 @@ conf::add_port(struct target *target, struct portal_group *pg, { std::string name = freebsd::stringf("%s-%s", pg->name(), target->name()); - const auto &pair = conf_ports.try_emplace(name, - std::make_unique<portal_group_port>(target, pg, ctl_port)); + const auto &pair = conf_ports.try_emplace(name, pg->create_port(target, + ctl_port)); if (!pair.second) { log_warnx("duplicate port \"%s\"", name.c_str()); return (false); @@ -1185,6 +1204,46 @@ portal_group::find_port(std::string_view target) const } 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) +{ + t_label = freebsd::stringf("%s \"%s\"", keyword, t_name.c_str()); +} + +struct target * conf::add_target(const char *name) { if (!valid_iscsi_name(name, log_warnx)) @@ -1198,7 +1257,7 @@ conf::add_target(const char *name) c = tolower(c); auto const &pair = conf_targets.try_emplace(t_name, - std::make_unique<target>(this, t_name)); + iscsi_make_target(this, t_name)); if (!pair.second) { log_warnx("duplicated target \"%s\"", name); return (NULL); @@ -1225,13 +1284,12 @@ target::use_private_auth(const char *keyword) return (true); if (t_auth_group != nullptr) { - log_warnx("cannot use both auth-group and %s for target \"%s\"", - keyword, name()); + log_warnx("cannot use both auth-group and %s for %s", + keyword, label()); return (false); } - std::string label = freebsd::stringf("target \"%s\"", name()); - t_auth_group = std::make_shared<struct auth_group>(label); + t_auth_group = std::make_shared<struct auth_group>(t_label); t_private_auth = true; return (true); } @@ -1254,40 +1312,24 @@ target::add_chap_mutual(const char *user, const char *secret, } bool -target::add_initiator_name(std::string_view name) -{ - if (!use_private_auth("initiator-name")) - return (false); - return (t_auth_group->add_initiator_name(name)); -} - -bool -target::add_initiator_portal(const char *addr) -{ - if (!use_private_auth("initiator-portal")) - return (false); - return (t_auth_group->add_initiator_portal(addr)); -} - -bool -target::add_lun(u_int id, const char *lun_name) +target::add_lun(u_int id, const char *lun_label, const char *lun_name) { struct lun *t_lun; if (id >= MAX_LUNS) { - log_warnx("LUN %u too big for target \"%s\"", id, name()); + log_warnx("%s too big for %s", lun_label, label()); return (false); } if (t_luns[id] != NULL) { - log_warnx("duplicate LUN %u for target \"%s\"", id, name()); + log_warnx("duplicate %s for %s", lun_label, label()); return (false); } t_lun = t_conf->find_lun(lun_name); if (t_lun == NULL) { - log_warnx("unknown LUN named %s used for target \"%s\"", - lun_name, name()); + log_warnx("unknown LUN named %s used for %s", lun_name, + label()); return (false); } @@ -1296,41 +1338,10 @@ target::add_lun(u_int id, const char *lun_name) } bool -target::add_portal_group(const char *pg_name, const char *ag_name) -{ - struct portal_group *pg; - auth_group_sp ag; - - pg = t_conf->find_portal_group(pg_name); - if (pg == NULL) { - log_warnx("unknown portal-group \"%s\" for target \"%s\"", - pg_name, name()); - return (false); - } - - if (ag_name != NULL) { - ag = t_conf->find_auth_group(ag_name); - if (ag == NULL) { - log_warnx("unknown auth-group \"%s\" for target \"%s\"", - ag_name, name()); - return (false); - } - } - - if (!t_conf->add_port(this, pg, std::move(ag))) { - log_warnx("can't link portal-group \"%s\" to target \"%s\"", - pg_name, name()); - return (false); - } - return (true); -} - -bool target::set_alias(std::string_view alias) { if (has_alias()) { - log_warnx("alias for target \"%s\" specified more than once", - name()); + log_warnx("alias for %s specified more than once", label()); return (false); } t_alias = alias; @@ -1343,16 +1354,16 @@ target::set_auth_group(const char *ag_name) if (t_auth_group != nullptr) { if (t_private_auth) log_warnx("cannot use both auth-group and explicit " - "authorisations for target \"%s\"", name()); + "authorisations for %s", label()); else - log_warnx("auth-group for target \"%s\" " - "specified more than once", name()); + log_warnx("auth-group for %s " + "specified more than once", label()); return (false); } t_auth_group = t_conf->find_auth_group(ag_name); if (t_auth_group == nullptr) { - log_warnx("unknown auth-group \"%s\" for target \"%s\"", - ag_name, name()); + log_warnx("unknown auth-group \"%s\" for %s", + ag_name, label()); return (false); } return (true); @@ -1383,8 +1394,8 @@ target::set_redirection(const char *addr) { if (!t_redirection.empty()) { log_warnx("cannot set redirection to \"%s\" for " - "target \"%s\"; already defined", - addr, name()); + "%s; already defined", + addr, label()); return (false); } @@ -1393,28 +1404,23 @@ target::set_redirection(const char *addr) } struct lun * -target::start_lun(u_int id) +target::start_lun(u_int id, const char *lun_label, const char *lun_name) { - struct lun *new_lun; - if (id >= MAX_LUNS) { - log_warnx("LUN %u too big for target \"%s\"", id, - name()); + log_warnx("%s too big for %s", lun_label, label()); return (nullptr); } if (t_luns[id] != NULL) { - log_warnx("duplicate LUN %u for target \"%s\"", id, - name()); + log_warnx("duplicate %s for %s", lun_label, label()); return (nullptr); } - std::string lun_name = freebsd::stringf("%s,lun,%u", name(), id); - new_lun = t_conf->add_lun(lun_name.c_str()); + struct lun *new_lun = t_conf->add_lun(lun_name); if (new_lun == nullptr) return (nullptr); - new_lun->set_scsiname(lun_name.c_str()); + new_lun->set_scsiname(lun_name); t_luns[id] = new_lun; @@ -1449,7 +1455,7 @@ target::verify() assert(t_auth_group != nullptr); } if (t_ports.empty()) { - struct portal_group *pg = t_conf->find_portal_group("default"); + struct portal_group *pg = default_portal_group(); assert(pg != NULL); t_conf->add_port(this, pg, nullptr); } @@ -1457,10 +1463,10 @@ target::verify() bool found = std::any_of(t_luns.begin(), t_luns.end(), [](struct lun *lun) { return (lun != nullptr); }); if (!found && t_redirection.empty()) - log_warnx("no LUNs defined for target \"%s\"", name()); + log_warnx("no LUNs defined for %s", label()); if (found && !t_redirection.empty()) - log_debugx("target \"%s\" contains luns, but configured " - "for redirection", name()); + log_debugx("%s contains LUNs, but configured " + "for redirection", label()); } lun::lun(struct conf *conf, std::string_view name) @@ -1485,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 * @@ -1714,59 +1722,6 @@ option_new(nvlist_t *nvl, const char *name, const char *value) return (true); } -#ifdef ICL_KERNEL_PROXY - -static void -pdu_receive_proxy(struct pdu *pdu) -{ - struct connection *conn; - size_t len; - - assert(proxy_mode); - conn = pdu->pdu_connection; - - kernel_receive(pdu); - - len = pdu_ahs_length(pdu); - if (len > 0) - log_errx(1, "protocol error: non-empty AHS"); - - len = pdu_data_segment_length(pdu); - assert(len <= (size_t)conn->conn_max_recv_data_segment_length); - pdu->pdu_data_len = len; -} - -static void -pdu_send_proxy(struct pdu *pdu) -{ - - assert(proxy_mode); - - pdu_set_data_segment_length(pdu, pdu->pdu_data_len); - kernel_send(pdu); -} - -#endif /* ICL_KERNEL_PROXY */ - -static void -pdu_fail(const struct connection *conn __unused, const char *reason __unused) -{ -} - -ctld_connection::ctld_connection(struct portal *portal, int fd, - const char *host, const struct sockaddr *client_sa) : - conn_portal(portal), conn_initiator_addr(host), - conn_initiator_sa(client_sa) -{ - connection_init(&conn, &conn_ops, proxy_mode); - conn.conn_socket = fd; -} - -ctld_connection::~ctld_connection() -{ - chap_delete(conn_chap); -} - bool lun::verify() { @@ -1838,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" || @@ -1882,23 +1843,23 @@ portal::init_socket() struct portal_group *pg = portal_group(); struct kevent kev; freebsd::fd_up s; - int error, sockbuf; + int error; int one = 1; #ifdef ICL_KERNEL_PROXY if (proxy_mode) { int id = pg->conf()->add_proxy_portal(this); - log_debugx("listening on %s, portal-group \"%s\", " - "portal id %d, using ICL proxy", listen(), pg->pg_name, - id); - kernel_listen(ai(), p_iser, id); + log_debugx("listening on %s, %s \"%s\", " + "portal id %d, using ICL proxy", listen(), pg->keyword(), + pg->name(), id); + kernel_listen(ai(), protocol() == ISER, id); return (true); } #endif assert(proxy_mode == false); - assert(p_iser == false); + assert(protocol() != portal_protocol::ISER); - log_debugx("listening on %s, portal-group \"%s\"", listen(), + log_debugx("listening on %s, %s \"%s\"", listen(), pg->keyword(), pg->name()); s = ::socket(p_ai->ai_family, p_ai->ai_socktype, p_ai->ai_protocol); if (!s) { @@ -1906,14 +1867,6 @@ portal::init_socket() return (false); } - sockbuf = SOCKBUF_SIZE; - if (setsockopt(s, SOL_SOCKET, SO_RCVBUF, &sockbuf, - sizeof(sockbuf)) == -1) - log_warn("setsockopt(SO_RCVBUF) failed for %s", listen()); - sockbuf = SOCKBUF_SIZE; - if (setsockopt(s, SOL_SOCKET, SO_SNDBUF, &sockbuf, - sizeof(sockbuf)) == -1) - log_warn("setsockopt(SO_SNDBUF) failed for %s", listen()); if (setsockopt(s, SOL_SOCKET, SO_NO_DDP, &one, sizeof(one)) == -1) log_warn("setsockopt(SO_NO_DDP) failed for %s", listen()); @@ -1960,6 +1913,9 @@ portal::init_socket() } } + if (!init_socket_options(s)) + return (false); + error = bind(s, p_ai->ai_addr, p_ai->ai_addrlen); if (error != 0) { log_warn("bind(2) failed for %s", listen()); @@ -1989,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); } @@ -2038,7 +2000,18 @@ conf::apply(struct conf *oldconf) if (it != oldconf->conf_portal_groups.end()) newpg.set_tag(it->second->tag()); else - newpg.set_tag(++last_portal_group_tag); + 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. */ @@ -2203,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. @@ -2210,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) { @@ -2225,7 +2204,7 @@ conf::apply(struct conf *oldconf) return (cumulated_error); } -static bool +bool timed_out(void) { @@ -2342,7 +2321,7 @@ wait_for_children(bool block) } static void -handle_connection(struct portal *portal, int fd, +handle_connection(struct portal *portal, freebsd::fd_up fd, const struct sockaddr *client_sa, bool dont_fork) { struct portal_group *pg; @@ -2373,10 +2352,8 @@ handle_connection(struct portal *portal, int fd, pid = fork(); if (pid < 0) log_err(1, "fork"); - if (pid > 0) { - close(fd); + if (pid > 0) return; - } conf->close_pidfile(); } @@ -2390,17 +2367,7 @@ handle_connection(struct portal *portal, int fd, log_set_peer_addr(host); setproctitle("%s", host); - ctld_connection conn(portal, fd, host, client_sa); - start_timer(conf->timeout(), true); - kernel_capsicate(); - conn.login(); - if (conn.session_type() == CONN_SESSION_TYPE_NORMAL) { - conn.kernel_handoff(); - log_debugx("connection handed off to the kernel"); - } else { - assert(conn.session_type() == CONN_SESSION_TYPE_DISCOVERY); - conn.discovery(); - } + portal->handle_connection(std::move(fd), host, client_sa); log_debugx("nothing more to do; exiting"); exit(0); } @@ -2585,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); @@ -2612,8 +2582,15 @@ conf_new_from_file(const char *path, bool ucl) "going with defaults"); pg = conf->find_portal_group("default"); assert(pg != NULL); - pg->add_portal("0.0.0.0", false); - pg->add_portal("[::]", false); + 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()) { @@ -2644,7 +2621,7 @@ conf::add_pports(struct kports &kports) if (ret > 0) { if (!add_port(kports, targ, i_pp, i_vp)) { log_warnx("can't create new ioctl port " - "for target \"%s\"", targ->name()); + "for %s", targ->label()); return (false); } @@ -2653,19 +2630,19 @@ conf::add_pports(struct kports &kports) pp = kports.find_port(targ->pport()); if (pp == NULL) { - log_warnx("unknown port \"%s\" for target \"%s\"", - targ->pport(), targ->name()); + log_warnx("unknown port \"%s\" for %s", + targ->pport(), targ->label()); return (false); } if (pp->linked()) { - log_warnx("can't link port \"%s\" to target \"%s\", " + log_warnx("can't link port \"%s\" to %s, " "port already linked to some target", - targ->pport(), targ->name()); + targ->pport(), targ->label()); return (false); } if (!add_port(targ, pp)) { - log_warnx("can't link port \"%s\" to target \"%s\"", - targ->pport(), targ->name()); + log_warnx("can't link port \"%s\" to %s", + targ->pport(), targ->label()); return (false); } } diff --git a/usr.sbin/ctld/ctld.hh b/usr.sbin/ctld/ctld.hh index 059f719668f9..bfe4507bb3e6 100644 --- a/usr.sbin/ctld/ctld.hh +++ b/usr.sbin/ctld/ctld.hh @@ -56,7 +56,6 @@ #define DEFAULT_CD_BLOCKSIZE 2048 #define MAX_LUNS 1024 -#define SOCKBUF_SIZE 1048576 struct isns_req; struct port; @@ -111,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; @@ -124,24 +129,38 @@ 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_names; - std::list<auth_portal> ag_portals; + 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; }; using auth_group_sp = std::shared_ptr<auth_group>; +enum class portal_protocol { + ISCSI, + ISER, + NVME_TCP, + NVME_DISCOVERY_TCP, +}; + struct portal { - portal(struct portal_group *pg, std::string_view listen, bool iser, - freebsd::addrinfo_up ai) : + portal(struct portal_group *pg, std::string_view listen, + portal_protocol protocol, freebsd::addrinfo_up ai) : p_portal_group(pg), p_listen(listen), p_ai(std::move(ai)), - p_iser(iser) {} + p_protocol(protocol) {} + virtual ~portal() = default; bool reuse_socket(portal &oldp); bool init_socket(); + virtual bool init_socket_options(int s __unused) { return true; } + 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; } + struct 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; } int socket() const { return p_socket; } void close() { p_socket.reset(); } @@ -149,12 +168,13 @@ private: struct portal_group *p_portal_group; std::string p_listen; freebsd::addrinfo_up p_ai; - bool p_iser; + portal_protocol p_protocol; freebsd::fd_up p_socket; }; using portal_up = std::unique_ptr<portal>; +using port_up = std::unique_ptr<port>; enum class discovery_filter { UNKNOWN, @@ -166,15 +186,17 @@ enum class discovery_filter { struct portal_group { portal_group(struct conf *conf, std::string_view name); + virtual ~portal_group() = default; struct conf *conf() const { return pg_conf; } + virtual const char *keyword() const = 0; const char *name() const { return pg_name.c_str(); } bool assigned() const { return pg_assigned; } bool is_dummy() const; bool is_redirecting() const { return !pg_redirection.empty(); } struct auth_group *discovery_auth_group() const { return pg_discovery_auth_group.get(); } - discovery_filter discovery_filter() const + enum discovery_filter discovery_filter() const { return pg_discovery_filter; } int dscp() const { return pg_dscp; } const char *offload() const { return pg_offload.c_str(); } @@ -188,17 +210,25 @@ struct portal_group { const std::unordered_map<std::string, port *> &ports() const { return pg_ports; } - bool add_portal(const char *value, bool iser); + virtual void allocate_tag() = 0; + virtual bool add_portal(const char *value, + portal_protocol protocol) = 0; + virtual void add_default_portals() = 0; bool add_option(const char *name, const char *value); bool set_discovery_auth_group(const char *name); bool set_dscp(u_int dscp); - bool set_filter(const char *str); + virtual bool set_filter(const char *str) = 0; void set_foreign(); bool set_offload(const char *offload); bool set_pcp(u_int pcp); bool set_redirection(const char *addr); void set_tag(uint16_t tag); + virtual port_up create_port(struct target *target, auth_group_sp ag) = + 0; + virtual port_up create_port(struct target *target, uint32_t ctl_port) = + 0; + void add_port(struct portal_group_port *port); const struct port *find_port(std::string_view target) const; void remove_port(struct portal_group_port *port); @@ -208,9 +238,10 @@ struct portal_group { int open_sockets(struct conf &oldconf); void close_sockets(); -private: +protected: struct conf *pg_conf; freebsd::nvlist_up pg_options; + const char *pg_keyword; std::string pg_name; auth_group_sp pg_discovery_auth_group; enum discovery_filter pg_discovery_filter = @@ -254,7 +285,7 @@ protected: uint32_t p_ctl_port = 0; }; -struct portal_group_port final : public port { +struct portal_group_port : public port { portal_group_port(struct target *target, struct portal_group *pg, auth_group_sp ag); portal_group_port(struct target *target, struct portal_group *pg, @@ -270,10 +301,7 @@ struct portal_group_port final : public port { void clear_references() override; - bool kernel_create_port() override; - bool kernel_remove_port() override; - -private: +protected: auth_group_sp p_auth_group; struct portal_group *p_portal_group; }; @@ -348,14 +376,15 @@ private: }; struct target { - target(struct conf *conf, std::string_view name) : - t_conf(conf), t_name(name) {} + target(struct conf *conf, const char *keyword, std::string_view name); + virtual ~target() = default; bool has_alias() const { return !t_alias.empty(); } bool has_pport() const { return !t_pport.empty(); } bool has_redirection() const { return !t_redirection.empty(); } const char *alias() const { return t_alias.c_str(); } const char *name() const { return t_name.c_str(); } + const char *label() const { return t_label.c_str(); } const char *pport() const { return t_pport.c_str(); } bool private_auth() const { return t_private_auth; } const char *redirection() const { return t_redirection.c_str(); } @@ -367,30 +396,40 @@ 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); - bool add_initiator_name(std::string_view name); - bool add_initiator_portal(const char *addr); - bool add_lun(u_int id, const char *lun_name); - bool add_portal_group(const char *pg_name, const char *ag_name); + 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); bool set_auth_group(const char *ag_name); bool set_auth_type(const char *type); bool set_physical_port(std::string_view pport); bool set_redirection(const char *addr); - struct lun *start_lun(u_int id); + 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); void remove_port(struct port *port); void verify(); -private: +protected: bool use_private_auth(const char *keyword); + bool add_lun(u_int id, const char *lun_label, const char *lun_name); + struct lun *start_lun(u_int id, const char *lun_label, + const char *lun_name); + virtual struct portal_group *default_portal_group() = 0; struct conf *t_conf; std::array<struct lun *, MAX_LUNS> t_luns; auth_group_sp t_auth_group; std::list<port *> t_ports; std::string t_name; + std::string t_label; std::string t_alias; std::string t_redirection; /* Name of this target's physical port, if any, i.e. "isp0" */ @@ -398,6 +437,8 @@ private: bool t_private_auth; }; +using target_up = std::unique_ptr<target>; + struct isns { isns(std::string_view addr, freebsd::addrinfo_up ai) : i_addr(addr), i_ai(std::move(ai)) {} @@ -413,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(); @@ -429,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, @@ -438,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); @@ -474,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; @@ -485,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 *); @@ -528,50 +587,7 @@ private: std::unordered_map<std::string, struct pport> pports; }; -#define CONN_SESSION_TYPE_NONE 0 -#define CONN_SESSION_TYPE_DISCOVERY 1 -#define CONN_SESSION_TYPE_NORMAL 2 - -struct ctld_connection { - ctld_connection(struct portal *portal, int fd, const char *host, - const struct sockaddr *client_sa); - ~ctld_connection(); - - int session_type() const { return conn_session_type; } - - void login(); - void discovery(); - void kernel_handoff(); -private: - void login_chap(struct auth_group *ag); - void login_negotiate_key(struct pdu *request, const char *name, - const char *value, bool skipped_security, - struct keys *response_keys); - bool login_portal_redirect(struct pdu *request); - bool login_target_redirect(struct pdu *request); - void login_negotiate(struct pdu *request); - void login_wait_transition(); - - bool discovery_target_filtered_out(const struct port *port) const; - - struct connection conn; - struct portal *conn_portal = nullptr; - const struct port *conn_port = nullptr; - struct target *conn_target = nullptr; - int conn_session_type = CONN_SESSION_TYPE_NONE; - std::string conn_initiator_name; - std::string conn_initiator_addr; - std::string conn_initiator_alias; - uint8_t conn_initiator_isid[6]; - const struct sockaddr *conn_initiator_sa = nullptr; - int conn_max_recv_data_segment_limit = 0; - int conn_max_send_data_segment_limit = 0; - int conn_max_burst_limit = 0; - int conn_first_burst_limit = 0; - std::string conn_user; - struct chap *conn_chap = nullptr; -}; - +extern bool proxy_mode; extern int ctl_fd; bool parse_conf(const char *path); @@ -584,6 +600,9 @@ void conf_start(struct conf *new_conf); bool option_new(nvlist_t *nvl, const char *name, const char *value); +freebsd::addrinfo_up parse_addr_port(const char *address, + const char *def_port); + void kernel_init(void); void kernel_capsicate(void); @@ -597,7 +616,22 @@ void kernel_send(struct pdu *pdu); void kernel_receive(struct pdu *pdu); #endif +bool ctl_create_port(const char *driver, + const nvlist_t *nvl, uint32_t *ctl_port); +bool ctl_remove_port(const char *driver, nvlist_t *nvl); + +portal_group_up iscsi_make_portal_group(struct conf *conf, + std::string_view name); +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(); #endif /* !__CTLD_HH__ */ diff --git a/usr.sbin/ctld/discovery.cc b/usr.sbin/ctld/discovery.cc index 3c9f1cfa1dac..8f6d371b696d 100644 --- a/usr.sbin/ctld/discovery.cc +++ b/usr.sbin/ctld/discovery.cc @@ -39,6 +39,7 @@ #include <sys/socket.h> #include "ctld.hh" +#include "iscsi.hh" #include "iscsi_proto.h" static struct pdu * @@ -112,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), @@ -145,7 +149,7 @@ discovery_add_target(struct keys *response_keys, const struct target *targ) } bool -ctld_connection::discovery_target_filtered_out(const struct port *port) const +iscsi_connection::discovery_target_filtered_out(const struct port *port) const { const struct auth_group *ag; const struct portal_group *pg; @@ -208,7 +212,7 @@ ctld_connection::discovery_target_filtered_out(const struct port *port) const } void -ctld_connection::discovery() +iscsi_connection::discovery() { struct pdu *request, *response; struct keys *request_keys, *response_keys; diff --git a/usr.sbin/ctld/iscsi.cc b/usr.sbin/ctld/iscsi.cc new file mode 100644 index 000000000000..bee036b95bf2 --- /dev/null +++ b/usr.sbin/ctld/iscsi.cc @@ -0,0 +1,508 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2003, 2004 Silicon Graphics International Corp. + * Copyright (c) 1997-2007 Kenneth D. Merry + * Copyright (c) 2012 The FreeBSD Foundation + * Copyright (c) 2017 Jakub Wojciech Klama <jceel@FreeBSD.org> + * All rights reserved. + * Copyright (c) 2025 Chelsio Communications, Inc. + * + * Portions of this software were developed by Edward Tomasz Napierala + * under sponsorship from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer, + * without modification. + * 2. Redistributions in binary form must reproduce at minimum a disclaimer + * substantially similar to the "NO WARRANTY" disclaimer below + * ("Disclaimer") and any redistribution must be conditioned upon + * including a substantially similar Disclaimer requirement for further + * binary redistribution. + * + * NO WARRANTY + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDERS OR CONTRIBUTORS BE LIABLE FOR SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGES. + * + */ + +#include <sys/param.h> +#include <sys/linker.h> +#include <sys/module.h> +#include <sys/time.h> +#include <assert.h> +#include <libiscsiutil.h> +#include <stdio.h> +#include <stdlib.h> +#include <cam/ctl/ctl.h> +#include <cam/ctl/ctl_io.h> +#include <cam/ctl/ctl_ioctl.h> + +#include "ctld.hh" +#include "iscsi.hh" + +#define SOCKBUF_SIZE 1048576 + +struct iscsi_portal final : public portal { + iscsi_portal(struct portal_group *pg, const char *listen, + portal_protocol protocol, freebsd::addrinfo_up ai) : + portal(pg, listen, protocol, std::move(ai)) {} + + bool init_socket_options(int s) override; + void handle_connection(freebsd::fd_up fd, const char *host, + const struct sockaddr *client_sa) override; +}; + +struct iscsi_portal_group final : public portal_group { + iscsi_portal_group(struct conf *conf, std::string_view name) : + portal_group(conf, name) {} + + const char *keyword() const override + { return "portal-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: + static uint16_t last_portal_group_tag; +}; + +struct iscsi_port final : public portal_group_port { + iscsi_port(struct target *target, struct portal_group *pg, + auth_group_sp ag) : + portal_group_port(target, pg, ag) {} + iscsi_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 module_loaded; + static void load_kernel_module(); +}; + +struct iscsi_target final : public target { + iscsi_target(struct conf *conf, std::string_view name) : + target(conf, "target", name) {} + + bool add_initiator_name(std::string_view name) override; + bool add_initiator_portal(const char *addr) override; + bool add_lun(u_int id, const char *lun_name) override; + bool add_portal_group(const char *pg_name, const char *ag_name) + override; + struct lun *start_lun(u_int id) override; + +protected: + struct portal_group *default_portal_group() override; +}; + +#ifdef ICL_KERNEL_PROXY +static void pdu_receive_proxy(struct pdu *pdu); +static void pdu_send_proxy(struct pdu *pdu); +#endif /* ICL_KERNEL_PROXY */ +static void pdu_fail(const struct connection *conn, const char *reason); + +uint16_t iscsi_portal_group::last_portal_group_tag = 0xff; +bool iscsi_port::module_loaded = false; + +static struct connection_ops conn_ops = { + .timed_out = timed_out, +#ifdef ICL_KERNEL_PROXY + .pdu_receive_proxy = pdu_receive_proxy, + .pdu_send_proxy = pdu_send_proxy, +#else + .pdu_receive_proxy = nullptr, + .pdu_send_proxy = nullptr, +#endif + .fail = pdu_fail, +}; + +portal_group_up +iscsi_make_portal_group(struct conf *conf, std::string_view name) +{ + return std::make_unique<iscsi_portal_group>(conf, name); +} + +target_up +iscsi_make_target(struct conf *conf, std::string_view name) +{ + return std::make_unique<iscsi_target>(conf, name); +} + +void +iscsi_portal_group::allocate_tag() +{ + set_tag(++last_portal_group_tag); +} + +bool +iscsi_portal_group::add_portal(const char *value, portal_protocol protocol) +{ + switch (protocol) { + case portal_protocol::ISCSI: + case portal_protocol::ISER: + break; + default: + log_warnx("unsupported portal protocol for %s", value); + return (false); + } + + freebsd::addrinfo_up ai = parse_addr_port(value, "3260"); + if (!ai) { + log_warnx("invalid listen address %s", value); + return (false); + } + + /* + * XXX: getaddrinfo(3) may return multiple addresses; we should turn + * those into multiple portals. + */ + + pg_portals.emplace_back(std::make_unique<iscsi_portal>(this, value, + protocol, std::move(ai))); + return (true); +} + +void +iscsi_portal_group::add_default_portals() +{ + add_portal("0.0.0.0", portal_protocol::ISCSI); + add_portal("[::]", portal_protocol::ISCSI); +} + +bool +iscsi_portal_group::set_filter(const char *str) +{ + enum discovery_filter filter; + + if (strcmp(str, "none") == 0) { + filter = discovery_filter::NONE; + } else if (strcmp(str, "portal") == 0) { + filter = discovery_filter::PORTAL; + } else if (strcmp(str, "portal-name") == 0) { + filter = discovery_filter::PORTAL_NAME; + } else if (strcmp(str, "portal-name-auth") == 0) { + filter = discovery_filter::PORTAL_NAME_AUTH; + } else { + log_warnx("invalid discovery-filter \"%s\" for portal-group " + "\"%s\"; valid values are \"none\", \"portal\", " + "\"portal-name\", and \"portal-name-auth\"", + str, name()); + return (false); + } + + if (pg_discovery_filter != discovery_filter::UNKNOWN && + pg_discovery_filter != filter) { + log_warnx("cannot set discovery-filter to \"%s\" for " + "portal-group \"%s\"; already has a different " + "value", str, name()); + return (false); + } + + pg_discovery_filter = filter; + return (true); +} + +port_up +iscsi_portal_group::create_port(struct target *target, auth_group_sp ag) +{ + return std::make_unique<iscsi_port>(target, this, ag); +} + +port_up +iscsi_portal_group::create_port(struct target *target, uint32_t ctl_port) +{ + return std::make_unique<iscsi_port>(target, this, ctl_port); +} + +void +iscsi_port::load_kernel_module() +{ + int saved_errno; + + if (module_loaded) + return; + + saved_errno = errno; + if (modfind("cfiscsi") == -1 && kldload("cfiscsi") == -1) + log_warn("couldn't load cfiscsi"); + errno = saved_errno; + module_loaded = true; +} + +bool +iscsi_port::kernel_create_port() +{ + struct portal_group *pg = p_portal_group; + struct target *targ = p_target; + + load_kernel_module(); + + freebsd::nvlist_up nvl = pg->options(); + nvlist_add_string(nvl.get(), "cfiscsi_target", targ->name()); + nvlist_add_string(nvl.get(), "ctld_portal_group_name", pg->name()); + nvlist_add_stringf(nvl.get(), "cfiscsi_portal_group_tag", "%u", + pg->tag()); + + if (targ->has_alias()) { + nvlist_add_string(nvl.get(), "cfiscsi_target_alias", + targ->alias()); + } + + return (ctl_create_port("iscsi", nvl.get(), &p_ctl_port)); +} + +bool +iscsi_port::kernel_remove_port() +{ + freebsd::nvlist_up nvl(nvlist_create(0)); + nvlist_add_string(nvl.get(), "cfiscsi_target", p_target->name()); + nvlist_add_stringf(nvl.get(), "cfiscsi_portal_group_tag", "%u", + p_portal_group->tag()); + + return (ctl_remove_port("iscsi", nvl.get())); +} + +bool +iscsi_portal::init_socket_options(int s) +{ + int sockbuf; + + sockbuf = SOCKBUF_SIZE; + if (setsockopt(s, SOL_SOCKET, SO_RCVBUF, &sockbuf, + sizeof(sockbuf)) == -1) { + log_warn("setsockopt(SO_RCVBUF) failed for %s", listen()); + return (false); + } + sockbuf = SOCKBUF_SIZE; + if (setsockopt(s, SOL_SOCKET, SO_SNDBUF, &sockbuf, + sizeof(sockbuf)) == -1) { + log_warn("setsockopt(SO_SNDBUF) failed for %s", listen()); + return (false); + } + return (true); +} + +bool +iscsi_target::add_initiator_name(std::string_view name) +{ + if (!use_private_auth("initiator-name")) + return (false); + return (t_auth_group->add_initiator_name(name)); +} + +bool +iscsi_target::add_initiator_portal(const char *addr) +{ + if (!use_private_auth("initiator-portal")) + return (false); + return (t_auth_group->add_initiator_portal(addr)); +} + +bool +iscsi_target::add_lun(u_int id, const char *lun_name) +{ + std::string lun_label = "LUN " + std::to_string(id); + return target::add_lun(id, lun_label.c_str(), lun_name); +} + +bool +iscsi_target::add_portal_group(const char *pg_name, const char *ag_name) +{ + struct portal_group *pg; + auth_group_sp ag; + + pg = t_conf->find_portal_group(pg_name); + if (pg == NULL) { + log_warnx("unknown portal-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 portal-group \"%s\" to %s", pg_name, + label()); + return (false); + } + return (true); +} + +struct lun * +iscsi_target::start_lun(u_int id) +{ + std::string lun_label = "LUN " + std::to_string(id); + std::string lun_name = freebsd::stringf("%s,lun,%u", name(), id); + return target::start_lun(id, lun_label.c_str(), lun_name.c_str()); +} + +struct portal_group * +iscsi_target::default_portal_group() +{ + return t_conf->find_portal_group("default"); +} + +#ifdef ICL_KERNEL_PROXY + +static void +pdu_receive_proxy(struct pdu *pdu) +{ + struct connection *conn; + size_t len; + + assert(proxy_mode); + conn = pdu->pdu_connection; + + kernel_receive(pdu); + + len = pdu_ahs_length(pdu); + if (len > 0) + log_errx(1, "protocol error: non-empty AHS"); + + len = pdu_data_segment_length(pdu); + assert(len <= (size_t)conn->conn_max_recv_data_segment_length); + pdu->pdu_data_len = len; +} + +static void +pdu_send_proxy(struct pdu *pdu) +{ + + assert(proxy_mode); + + pdu_set_data_segment_length(pdu, pdu->pdu_data_len); + kernel_send(pdu); +} + +#endif /* ICL_KERNEL_PROXY */ + +static void +pdu_fail(const struct connection *conn __unused, const char *reason __unused) +{ +} + +iscsi_connection::iscsi_connection(struct portal *portal, freebsd::fd_up fd, + const char *host, const struct sockaddr *client_sa) : + conn_portal(portal), conn_fd(std::move(fd)), conn_initiator_addr(host), + conn_initiator_sa(client_sa) +{ + connection_init(&conn, &conn_ops, proxy_mode); + conn.conn_socket = conn_fd; +} + +iscsi_connection::~iscsi_connection() +{ + chap_delete(conn_chap); +} + +void +iscsi_connection::kernel_handoff() +{ + struct portal_group *pg = conn_portal->portal_group(); + struct ctl_iscsi req; + + bzero(&req, sizeof(req)); + + req.type = CTL_ISCSI_HANDOFF; + strlcpy(req.data.handoff.initiator_name, conn_initiator_name.c_str(), + sizeof(req.data.handoff.initiator_name)); + strlcpy(req.data.handoff.initiator_addr, conn_initiator_addr.c_str(), + sizeof(req.data.handoff.initiator_addr)); + if (!conn_initiator_alias.empty()) { + strlcpy(req.data.handoff.initiator_alias, + conn_initiator_alias.c_str(), + sizeof(req.data.handoff.initiator_alias)); + } + memcpy(req.data.handoff.initiator_isid, conn_initiator_isid, + sizeof(req.data.handoff.initiator_isid)); + strlcpy(req.data.handoff.target_name, conn_target->name(), + sizeof(req.data.handoff.target_name)); + strlcpy(req.data.handoff.offload, pg->offload(), + sizeof(req.data.handoff.offload)); +#ifdef ICL_KERNEL_PROXY + if (proxy_mode) + req.data.handoff.connection_id = conn.conn_socket; + else + req.data.handoff.socket = conn.conn_socket; +#else + req.data.handoff.socket = conn.conn_socket; +#endif + req.data.handoff.portal_group_tag = pg->tag(); + if (conn.conn_header_digest == CONN_DIGEST_CRC32C) + req.data.handoff.header_digest = CTL_ISCSI_DIGEST_CRC32C; + if (conn.conn_data_digest == CONN_DIGEST_CRC32C) + req.data.handoff.data_digest = CTL_ISCSI_DIGEST_CRC32C; + req.data.handoff.cmdsn = conn.conn_cmdsn; + req.data.handoff.statsn = conn.conn_statsn; + req.data.handoff.max_recv_data_segment_length = + conn.conn_max_recv_data_segment_length; + req.data.handoff.max_send_data_segment_length = + conn.conn_max_send_data_segment_length; + req.data.handoff.max_burst_length = conn.conn_max_burst_length; + req.data.handoff.first_burst_length = conn.conn_first_burst_length; + req.data.handoff.immediate_data = conn.conn_immediate_data; + + if (ioctl(ctl_fd, CTL_ISCSI, &req) == -1) { + log_err(1, "error issuing CTL_ISCSI ioctl; " + "dropping connection"); + } + + if (req.status != CTL_ISCSI_OK) { + log_errx(1, "error returned from CTL iSCSI handoff request: " + "%s; dropping connection", req.error_str); + } +} + +void +iscsi_connection::handle() +{ + login(); + if (conn_session_type == CONN_SESSION_TYPE_NORMAL) { + kernel_handoff(); + log_debugx("connection handed off to the kernel"); + } else { + assert(conn_session_type == CONN_SESSION_TYPE_DISCOVERY); + discovery(); + } +} + +void +iscsi_portal::handle_connection(freebsd::fd_up fd, const char *host, + const struct sockaddr *client_sa) +{ + struct conf *conf = portal_group()->conf(); + + iscsi_connection conn(this, std::move(fd), host, client_sa); + start_timer(conf->timeout(), true); + kernel_capsicate(); + conn.handle(); +} diff --git a/usr.sbin/ctld/iscsi.hh b/usr.sbin/ctld/iscsi.hh new file mode 100644 index 000000000000..d510e8c6731b --- /dev/null +++ b/usr.sbin/ctld/iscsi.hh @@ -0,0 +1,79 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2012 The FreeBSD Foundation + * + * This software was developed by Edward Tomasz Napierala under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#ifndef __ISCSI_HH__ +#define __ISCSI_HH__ + +#define CONN_SESSION_TYPE_NONE 0 +#define CONN_SESSION_TYPE_DISCOVERY 1 +#define CONN_SESSION_TYPE_NORMAL 2 + +struct iscsi_connection { + iscsi_connection(struct portal *portal, freebsd::fd_up fd, + const char *host, const struct sockaddr *client_sa); + ~iscsi_connection(); + + void handle(); +private: + void login(); + void login_chap(struct auth_group *ag); + void login_negotiate_key(struct pdu *request, const char *name, + const char *value, bool skipped_security, + struct keys *response_keys); + bool login_portal_redirect(struct pdu *request); + bool login_target_redirect(struct pdu *request); + void login_negotiate(struct pdu *request); + void login_wait_transition(); + + void discovery(); + bool discovery_target_filtered_out(const struct port *port) const; + + void kernel_handoff(); + + struct connection conn; + struct portal *conn_portal = nullptr; + const struct port *conn_port = nullptr; + struct target *conn_target = nullptr; + freebsd::fd_up conn_fd; + int conn_session_type = CONN_SESSION_TYPE_NONE; + std::string conn_initiator_name; + std::string conn_initiator_addr; + std::string conn_initiator_alias; + uint8_t conn_initiator_isid[6]; + const struct sockaddr *conn_initiator_sa = nullptr; + int conn_max_recv_data_segment_limit = 0; + int conn_max_send_data_segment_limit = 0; + int conn_max_burst_limit = 0; + int conn_first_burst_limit = 0; + std::string conn_user; + struct chap *conn_chap = nullptr; +}; + +#endif /* !__ISCSI_HH__ */ diff --git a/usr.sbin/ctld/kernel.cc b/usr.sbin/ctld/kernel.cc index 39bbb4290e84..6b17ce60ac69 100644 --- a/usr.sbin/ctld/kernel.cc +++ b/usr.sbin/ctld/kernel.cc @@ -75,8 +75,6 @@ #define NVLIST_BUFSIZE 1024 -extern bool proxy_mode; - int ctl_fd = 0; void @@ -108,7 +106,7 @@ kernel_init(void) /* * Backend LUN information. */ -using attr_list = std::list<std::pair<std::string, std::string>>; +using attr_list_t = std::list<std::pair<std::string, std::string>>; struct cctl_lun { uint64_t lun_id; @@ -119,7 +117,7 @@ struct cctl_lun { std::string serial_number; std::string device_id; std::string ctld_name; - attr_list attr_list; + attr_list_t attr_list; }; struct cctl_port { @@ -128,11 +126,14 @@ 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; - attr_list attr_list; + std::string ctld_transport_group_name; + attr_list_t attr_list; }; struct cctl_devlist_data { @@ -324,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) { @@ -432,6 +441,93 @@ retry_port: return (true); } +void +add_iscsi_port(struct kports &kports, struct conf *conf, + const struct cctl_port &port, std::string &name) +{ + if (port.cfiscsi_target.empty()) { + log_debugx("CTL port %u \"%s\" wasn't managed by ctld; ", + port.port_id, name.c_str()); + if (!kports.has_port(name)) { + if (!kports.add_port(name, port.port_id)) { + log_warnx("kports::add_port failed"); + return; + } + } + return; + } + if (port.cfiscsi_state != 1) { + log_debugx("CTL port %ju is not active (%d); ignoring", + (uintmax_t)port.port_id, port.cfiscsi_state); + return; + } + + const char *t_name = port.cfiscsi_target.c_str(); + struct target *targ = conf->find_target(t_name); + if (targ == nullptr) { + targ = conf->add_target(t_name); + if (targ == nullptr) { + log_warnx("Failed to add target \"%s\"", t_name); + return; + } + } + + if (port.ctld_portal_group_name.empty()) + return; + + const char *pg_name = port.ctld_portal_group_name.c_str(); + struct portal_group *pg = conf->find_portal_group(pg_name); + if (pg == nullptr) { + pg = conf->add_portal_group(pg_name); + if (pg == nullptr) { + log_warnx("Failed to add portal-group \"%s\"", pg_name); + return; + } + } + pg->set_tag(port.cfiscsi_portal_group_tag); + if (!conf->add_port(targ, pg, port.port_id)) { + log_warnx("Failed to add port for target \"%s\" and portal-group \"%s\"", + t_name, pg_name); + } +} + +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) { @@ -455,51 +551,13 @@ conf_new_from_kernel(struct kports &kports) name += "/" + std::to_string(port.vp); } - if (port.cfiscsi_target.empty()) { - log_debugx("CTL port %u \"%s\" wasn't managed by ctld; ", - port.port_id, name.c_str()); - if (!kports.has_port(name)) { - if (!kports.add_port(name, port.port_id)) { - log_warnx("kports::add_port failed"); - continue; - } - } - continue; - } - if (port.cfiscsi_state != 1) { - log_debugx("CTL port %ju is not active (%d); ignoring", - (uintmax_t)port.port_id, port.cfiscsi_state); - continue; - } - - const char *t_name = port.cfiscsi_target.c_str(); - struct target *targ = conf->find_target(t_name); - if (targ == NULL) { - targ = conf->add_target(t_name); - if (targ == NULL) { - log_warnx("Failed to add target \"%s\"", - t_name); - continue; - } - } - - if (port.ctld_portal_group_name.empty()) - continue; - const char *pg_name = port.ctld_portal_group_name.c_str(); - struct portal_group *pg = conf->find_portal_group(pg_name); - if (pg == NULL) { - pg = conf->add_portal_group(pg_name); - if (pg == NULL) { - log_warnx("Failed to add portal_group \"%s\"", - pg_name); - continue; - } - } - pg->set_tag(port.cfiscsi_portal_group_tag); - if (!conf->add_port(targ, pg, port.port_id)) { - log_warnx("Failed to add port for target \"%s\" and portal-group \"%s\"", - t_name, pg_name); - continue; + 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); } } @@ -705,65 +763,7 @@ lun::kernel_remove() const return (true); } -void -ctld_connection::kernel_handoff() -{ - struct portal_group *pg = conn_portal->portal_group(); - struct ctl_iscsi req; - - bzero(&req, sizeof(req)); - - req.type = CTL_ISCSI_HANDOFF; - strlcpy(req.data.handoff.initiator_name, conn_initiator_name.c_str(), - sizeof(req.data.handoff.initiator_name)); - strlcpy(req.data.handoff.initiator_addr, conn_initiator_addr.c_str(), - sizeof(req.data.handoff.initiator_addr)); - if (!conn_initiator_alias.empty()) { - strlcpy(req.data.handoff.initiator_alias, - conn_initiator_alias.c_str(), - sizeof(req.data.handoff.initiator_alias)); - } - memcpy(req.data.handoff.initiator_isid, conn_initiator_isid, - sizeof(req.data.handoff.initiator_isid)); - strlcpy(req.data.handoff.target_name, conn_target->name(), - sizeof(req.data.handoff.target_name)); - strlcpy(req.data.handoff.offload, pg->offload(), - sizeof(req.data.handoff.offload)); -#ifdef ICL_KERNEL_PROXY - if (proxy_mode) - req.data.handoff.connection_id = conn.conn_socket; - else - req.data.handoff.socket = conn.conn_socket; -#else - req.data.handoff.socket = conn.conn_socket; -#endif - req.data.handoff.portal_group_tag = pg->tag(); - if (conn.conn_header_digest == CONN_DIGEST_CRC32C) - req.data.handoff.header_digest = CTL_ISCSI_DIGEST_CRC32C; - if (conn.conn_data_digest == CONN_DIGEST_CRC32C) - req.data.handoff.data_digest = CTL_ISCSI_DIGEST_CRC32C; - req.data.handoff.cmdsn = conn.conn_cmdsn; - req.data.handoff.statsn = conn.conn_statsn; - req.data.handoff.max_recv_data_segment_length = - conn.conn_max_recv_data_segment_length; - req.data.handoff.max_send_data_segment_length = - conn.conn_max_send_data_segment_length; - req.data.handoff.max_burst_length = conn.conn_max_burst_length; - req.data.handoff.first_burst_length = conn.conn_first_burst_length; - req.data.handoff.immediate_data = conn.conn_immediate_data; - - if (ioctl(ctl_fd, CTL_ISCSI, &req) == -1) { - log_err(1, "error issuing CTL_ISCSI ioctl; " - "dropping connection"); - } - - if (req.status != CTL_ISCSI_OK) { - log_errx(1, "error returned from CTL iSCSI handoff request: " - "%s; dropping connection", req.error_str); - } -} - -static bool +bool ctl_create_port(const char *driver, const nvlist_t *nvl, uint32_t *ctl_port) { struct ctl_req req; @@ -812,26 +812,6 @@ ctl_create_port(const char *driver, const nvlist_t *nvl, uint32_t *ctl_port) } bool -portal_group_port::kernel_create_port() -{ - struct portal_group *pg = p_portal_group; - struct target *targ = p_target; - - freebsd::nvlist_up nvl = pg->options(); - nvlist_add_string(nvl.get(), "cfiscsi_target", targ->name()); - nvlist_add_string(nvl.get(), "ctld_portal_group_name", pg->name()); - nvlist_add_stringf(nvl.get(), "cfiscsi_portal_group_tag", "%u", - pg->tag()); - - if (targ->has_alias()) { - nvlist_add_string(nvl.get(), "cfiscsi_target_alias", - targ->alias()); - } - - return (ctl_create_port("iscsi", nvl.get(), &p_ctl_port)); -} - -bool ioctl_port::kernel_create_port() { freebsd::nvlist_up nvl(nvlist_create(0)); @@ -971,17 +951,6 @@ ctl_remove_port(const char *driver, nvlist_t *nvl) } bool -portal_group_port::kernel_remove_port() -{ - freebsd::nvlist_up nvl(nvlist_create(0)); - nvlist_add_string(nvl.get(), "cfiscsi_target", p_target->name()); - nvlist_add_stringf(nvl.get(), "cfiscsi_portal_group_tag", "%u", - p_portal_group->tag()); - - return (ctl_remove_port("iscsi", nvl.get())); -} - -bool ioctl_port::kernel_remove_port() { freebsd::nvlist_up nvl(nvlist_create(0)); @@ -1141,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/login.cc b/usr.sbin/ctld/login.cc index aa62cd4c8b5b..cda11cc1f21b 100644 --- a/usr.sbin/ctld/login.cc +++ b/usr.sbin/ctld/login.cc @@ -42,6 +42,7 @@ #include <cam/ctl/ctl_ioctl.h> #include "ctld.hh" +#include "iscsi.hh" #include "iscsi_proto.h" #define MAX_DATA_SEGMENT_LENGTH (128 * 1024) @@ -455,7 +456,7 @@ login_send_chap_success(struct pdu *request, } void -ctld_connection::login_chap(struct auth_group *ag) +iscsi_connection::login_chap(struct auth_group *ag) { std::string user; const struct auth *auth; @@ -503,7 +504,7 @@ ctld_connection::login_chap(struct auth_group *ag) } void -ctld_connection::login_negotiate_key(struct pdu *request, const char *name, +iscsi_connection::login_negotiate_key(struct pdu *request, const char *name, const char *value, bool skipped_security, struct keys *response_keys) { int which; @@ -694,7 +695,7 @@ login_redirect(struct pdu *request, const char *target_address) } bool -ctld_connection::login_portal_redirect(struct pdu *request) +iscsi_connection::login_portal_redirect(struct pdu *request) { const struct portal_group *pg; @@ -710,7 +711,7 @@ ctld_connection::login_portal_redirect(struct pdu *request) } bool -ctld_connection::login_target_redirect(struct pdu *request) +iscsi_connection::login_target_redirect(struct pdu *request) { const char *target_address; @@ -731,7 +732,7 @@ ctld_connection::login_target_redirect(struct pdu *request) } void -ctld_connection::login_negotiate(struct pdu *request) +iscsi_connection::login_negotiate(struct pdu *request) { struct portal_group *pg = conn_portal->portal_group(); struct pdu *response; @@ -751,7 +752,7 @@ ctld_connection::login_negotiate(struct pdu *request) conn_max_burst_limit = (1 << 24) - 1; conn_first_burst_limit = (1 << 24) - 1; kernel_limits(pg->offload(), - conn.conn_socket, + conn_fd, &conn_max_recv_data_segment_limit, &conn_max_send_data_segment_limit, &conn_max_burst_limit, @@ -860,7 +861,7 @@ ctld_connection::login_negotiate(struct pdu *request) } void -ctld_connection::login_wait_transition() +iscsi_connection::login_wait_transition() { struct pdu *request, *response; struct iscsi_bhs_login_request *bhslr; @@ -884,7 +885,7 @@ ctld_connection::login_wait_transition() } void -ctld_connection::login() +iscsi_connection::login() { struct pdu *request, *response; struct iscsi_bhs_login_request *bhslr; 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..839b69fdac14 --- /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 struct 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..8a62636ffec6 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)) @@ -1037,7 +1418,7 @@ uclparse_conf(const char *path) bool parsed; try { parsed = uclparse_toplevel(top); - } catch (std::bad_alloc) { + } catch (std::bad_alloc &) { log_warnx("failed to allocate memory parsing %s", path); parsed = false; } catch (...) { |