Page MenuHomeFreeBSD

D48773.diff
No OneTemporary

D48773.diff

diff --git a/usr.sbin/ctld/Makefile b/usr.sbin/ctld/Makefile
--- a/usr.sbin/ctld/Makefile
+++ b/usr.sbin/ctld/Makefile
@@ -6,19 +6,21 @@
PACKAGE= ctl
PROG_CXX= ctld
SRCS= ctld.cc conf.cc discovery.cc iscsi.cc isns.cc kernel.cc
-SRCS+= login.cc parse.y token.l y.tab.h uclparse.cc
+SRCS+= login.cc nvmf.cc nvmf_discovery.cc
+SRCS+= parse.y token.l y.tab.h uclparse.cc
CFLAGS+= -I${.CURDIR}
CFLAGS+= -I${SRCTOP}/sys
CFLAGS+= -I${SRCTOP}/sys/cam/ctl
CFLAGS+= -I${SRCTOP}/sys/dev/iscsi
CFLAGS+= -I${SRCTOP}/lib/libiscsiutil
CFLAGS+= -I${SRCTOP}/lib/libutil++
+CFLAGS+= -I${SRCTOP}/lib/libnvmf
#CFLAGS+= -DICL_KERNEL_PROXY
NO_WCAST_ALIGN=
CXXWARNFLAGS.gcc= -Wno-shadow
MAN= ctld.8 ctl.conf.5
-LIBADD= bsdxml iscsiutil md sbuf util ucl m nv util++
+LIBADD= bsdxml iscsiutil nvmf md sbuf util ucl m nv util++
YFLAGS+= -v
CLEANFILES= y.tab.c y.tab.h y.output
diff --git a/usr.sbin/ctld/conf.h b/usr.sbin/ctld/conf.h
--- a/usr.sbin/ctld/conf.h
+++ b/usr.sbin/ctld/conf.h
@@ -43,6 +43,8 @@
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_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_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/conf.cc b/usr.sbin/ctld/conf.cc
--- a/usr.sbin/ctld/conf.cc
+++ b/usr.sbin/ctld/conf.cc
@@ -122,6 +122,18 @@
return (auth_group->add_chap_mutual(user, secret, user2, secret2));
}
+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)
{
@@ -233,6 +245,29 @@
portal_group->set_tag(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)
{
@@ -387,6 +422,38 @@
return (lun != nullptr);
}
+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)
{
diff --git a/usr.sbin/ctld/ctl.conf.5 b/usr.sbin/ctld/ctl.conf.5
--- a/usr.sbin/ctld/ctl.conf.5
+++ b/usr.sbin/ctld/ctl.conf.5
@@ -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 @@
.Dl ...
}
+.No transport-group Ar name No {
+.Dl listen Ar transport Ar address
+.Dl ...
+}
+
.No target Ar name {
.Dl auth-group Ar name
.Dl portal-group Ar name
@@ -67,6 +72,15 @@
.Dl }
.Dl ...
}
+
+.No controller Ar name {
+.Dl auth-group Ar name
+.Dl transport-group Ar name
+.Dl namespace Ar number No {
+.Dl path Ar path
+.Dl }
+.Dl ...
+}
.Ed
.Ss Global Context
.Bl -tag -width indent
@@ -94,16 +108,29 @@
configuration context,
defining a new portal-group,
which can then be assigned to any number of targets.
+.It Ic transport-group Ar name
+Create a
+.Sy transport-group
+configuration context,
+defining a new transport-group,
+which can then be assigned to any number of NVMeoF controllers.
.It Ic lun Ar name
Create a
.Sy lun
-configuration context, defining a LUN to be exported by any number of targets.
+configuration context, defining a LUN to be exported by any number of targets
+or controllers.
.It Ic target Ar name
Create a
.Sy target
configuration context, which can optionally contain one or more
.Sy lun
contexts.
+.It Ic controller Ar name
+Create a
+.Sy controller
+configuration context, which can optionally contain one or more
+.Sy namespace
+contexts.
.It Ic timeout Ar seconds
The timeout for login sessions, after which the connection
will be forcibly terminated.
@@ -150,6 +177,19 @@
or
.Sy chap-mutual
entries; it is an error to mix them.
+.It Ic host-address Ar address Ns Op / Ns Ar prefixlen
+An NVMeoF host address: an IPv4 or IPv6 address, optionally
+followed by a literal slash and a prefix length.
+Only NVMeoF hosts with an address matching one of the defined
+addresses will be allowed to connect.
+If not defined, there will be no restrictions based on host
+address.
+.It Ic host-nqn Ar name
+An NVMeoF host name.
+Only NVMeoF hosts with a name matching one of the defined
+names will be allowed to connect.
+If not defined, there will be no restrictions based on NVMe host
+name.
.It Ic initiator-name Ar initiator-name
An iSCSI initiator name.
Only initiators with a name matching one of the defined
@@ -264,6 +304,75 @@
.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 @@
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 @@
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 @@
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 @@
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 @@
}
}
}
+
+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.hh b/usr.sbin/ctld/ctld.hh
--- a/usr.sbin/ctld/ctld.hh
+++ b/usr.sbin/ctld/ctld.hh
@@ -110,6 +110,12 @@
const char *user2, const char *secret2);
const struct auth *find_auth(std::string_view user) const;
+ bool add_host_nqn(std::string_view nqn);
+ bool host_permitted(std::string_view nqn) const;
+
+ bool add_host_address(const char *address);
+ bool host_permitted(const struct sockaddr *sa) const;
+
bool add_initiator_name(std::string_view initiator_name);
bool initiator_permitted(std::string_view initiator_name) const;
@@ -123,6 +129,8 @@
std::string ag_label;
auth_type ag_type = auth_type::UNKNOWN;
std::unordered_map<std::string, auth> ag_auths;
+ std::unordered_set<std::string> ag_host_names;
+ std::list<auth_portal> ag_host_addresses;
std::unordered_set<std::string> ag_initiator_names;
std::list<auth_portal> ag_initiator_portals;
};
@@ -131,7 +139,9 @@
enum class portal_protocol {
ISCSI,
- ISER
+ ISER,
+ NVME_TCP,
+ NVME_DISCOVERY_TCP,
};
struct portal {
@@ -147,7 +157,7 @@
virtual void handle_connection(freebsd::fd_up fd, const char *host,
const struct sockaddr *client_sa) = 0;
- portal_group *portal_group() { return p_portal_group; }
+ portal_group *portal_group() const { return p_portal_group; }
const char *listen() const { return p_listen.c_str(); }
const addrinfo *ai() const { return p_ai.get(); }
portal_protocol protocol() const { return p_protocol; }
@@ -386,9 +396,12 @@
bool add_chap(const char *user, const char *secret);
bool add_chap_mutual(const char *user, const char *secret,
const char *user2, const char *secret2);
+ virtual bool add_host_address(const char *) { return false; }
+ virtual bool add_host_nqn(std::string_view) { return false; }
virtual bool add_initiator_name(std::string_view) { return false; }
virtual bool add_initiator_portal(const char *) { return false; }
virtual bool add_lun(u_int, const char *) { return false; }
+ virtual bool add_namespace(u_int, const char *) { return false; }
virtual bool add_portal_group(const char *pg_name,
const char *ag_name) = 0;
bool set_alias(std::string_view alias);
@@ -397,6 +410,7 @@
bool set_physical_port(std::string_view pport);
bool set_redirection(const char *addr);
virtual struct lun *start_lun(u_int) { return nullptr; }
+ virtual struct lun *start_namespace(u_int) { return nullptr; }
void add_port(struct port *port);
void remove_lun(struct lun *lun);
@@ -440,13 +454,18 @@
};
struct conf {
+ conf();
+
int maxproc() const { return conf_maxproc; }
int timeout() const { return conf_timeout; }
+ uint32_t genctr() const { return conf_genctr; }
bool default_auth_group_defined() const
{ return conf_default_ag_defined; }
bool default_portal_group_defined() const
{ return conf_default_pg_defined; }
+ bool default_transport_group_defined() const
+ { return conf_default_tg_defined; }
struct auth_group *add_auth_group(const char *ag_name);
struct auth_group *define_default_auth_group();
@@ -456,6 +475,10 @@
struct portal_group *define_default_portal_group();
struct portal_group *find_portal_group(std::string_view name);
+ struct portal_group *add_transport_group(const char *name);
+ struct portal_group *define_default_transport_group();
+ struct portal_group *find_transport_group(std::string_view name);
+
bool add_port(struct target *target, struct portal_group *pg,
auth_group_sp ag);
bool add_port(struct target *target, struct portal_group *pg,
@@ -465,6 +488,9 @@
int vp);
bool add_pports(struct kports &kports);
+ struct target *add_controller(const char *name);
+ struct target *find_controller(std::string_view name);
+
struct target *add_target(const char *name);
struct target *find_target(std::string_view name);
@@ -501,10 +527,12 @@
std::string conf_pidfile_path;
std::unordered_map<std::string, std::unique_ptr<lun>> conf_luns;
- std::unordered_map<std::string, std::unique_ptr<target>> conf_targets;
+ std::unordered_map<std::string, target_up> conf_targets;
+ std::unordered_map<std::string, target_up> conf_controllers;
std::unordered_map<std::string, auth_group_sp> conf_auth_groups;
std::unordered_map<std::string, std::unique_ptr<port>> conf_ports;
std::unordered_map<std::string, portal_group_up> conf_portal_groups;
+ std::unordered_map<std::string, portal_group_up> conf_transport_groups;
std::unordered_map<std::string, isns> conf_isns;
struct target *conf_first_target = nullptr;
int conf_isns_period = 900;
@@ -512,12 +540,16 @@
int conf_debug = 0;
int conf_timeout = 60;
int conf_maxproc = 30;
+ uint32_t conf_genctr = 0;
freebsd::pidfile conf_pidfile;
bool conf_default_pg_defined = false;
+ bool conf_default_tg_defined = false;
bool conf_default_ag_defined = false;
+ static uint32_t global_genctr;
+
#ifdef ICL_KERNEL_PROXY
public:
int add_proxy_portal(portal *);
@@ -593,6 +625,11 @@
target_up iscsi_make_target(struct conf *conf,
std::string_view name);
+portal_group_up nvmf_make_transport_group(struct conf *conf,
+ std::string_view name);
+target_up nvmf_make_controller(struct conf *conf,
+ std::string_view name);
+
void start_timer(int timeout, bool fatal = false);
void stop_timer();
bool timed_out();
diff --git a/usr.sbin/ctld/ctld.cc b/usr.sbin/ctld/ctld.cc
--- a/usr.sbin/ctld/ctld.cc
+++ b/usr.sbin/ctld/ctld.cc
@@ -41,6 +41,7 @@
#include <assert.h>
#include <ctype.h>
#include <errno.h>
+#include <libnvmf.h>
#include <netdb.h>
#include <signal.h>
#include <stdbool.h>
@@ -67,6 +68,8 @@
static int kqfd;
static int nchildren = 0;
+uint32_t conf::global_genctr;
+
static void
usage(void)
{
@@ -76,6 +79,11 @@
exit(1);
}
+conf::conf()
+{
+ conf_genctr = global_genctr++;
+}
+
void
conf::set_debug(int debug)
{
@@ -277,6 +285,23 @@
return (true);
}
+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)
{
@@ -360,6 +385,20 @@
return true;
}
+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)
{
@@ -406,6 +445,18 @@
return (true);
}
+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
{
@@ -501,6 +552,45 @@
return (it->second.get());
}
+struct portal_group *
+conf::add_transport_group(const char *name)
+{
+ auto pair = conf_transport_groups.try_emplace(name,
+ nvmf_make_transport_group(this, name));
+ if (!pair.second) {
+ log_warnx("duplicated transport-group \"%s\"", name);
+ return (nullptr);
+ }
+
+ return (pair.first->second.get());
+}
+
+/*
+ * Make it possible to redefine the default transport-group, but only
+ * once.
+ */
+struct portal_group *
+conf::define_default_transport_group()
+{
+ if (conf_default_tg_defined) {
+ log_warnx("duplicated transport-group \"default\"");
+ return (nullptr);
+ }
+
+ conf_default_tg_defined = true;
+ return (find_transport_group("default"));
+}
+
+struct portal_group *
+conf::find_transport_group(std::string_view name)
+{
+ auto it = conf_transport_groups.find(std::string(name));
+ if (it == conf_transport_groups.end())
+ return (nullptr);
+
+ return (it->second.get());
+}
+
bool
portal_group::is_dummy() const
{
@@ -1113,6 +1203,40 @@
return (it->second);
}
+struct target *
+conf::add_controller(const char *name)
+{
+ if (!nvmf_nqn_valid_strict(name)) {
+ log_warnx("controller name \"%s\" is invalid for NVMe", name);
+ return nullptr;
+ }
+
+ /*
+ * Normalize the name to lowercase to match iSCSI.
+ */
+ std::string t_name(name);
+ for (char &c : t_name)
+ c = tolower(c);
+
+ auto const &pair = conf_controllers.try_emplace(t_name,
+ nvmf_make_controller(this, t_name));
+ if (!pair.second) {
+ log_warnx("duplicated controller \"%s\"", name);
+ return nullptr;
+ }
+
+ return pair.first->second.get();
+}
+
+struct target *
+conf::find_controller(std::string_view name)
+{
+ auto it = conf_controllers.find(std::string(name));
+ if (it == conf_controllers.end())
+ return nullptr;
+ return it->second.get();
+}
+
target::target(struct conf *conf, const char *keyword, std::string_view name) :
t_conf(conf), t_name(name)
{
@@ -1367,6 +1491,8 @@
{
for (const auto &kv : conf_targets)
kv.second->remove_lun(lun);
+ for (const auto &kv : conf_controllers)
+ kv.second->remove_lun(lun);
}
struct lun *
@@ -1667,9 +1793,15 @@
for (auto &kv : conf_targets) {
kv.second->verify();
}
+ for (auto &kv : conf_controllers) {
+ kv.second->verify();
+ }
for (auto &kv : conf_portal_groups) {
kv.second->verify(this);
}
+ for (auto &kv : conf_transport_groups) {
+ kv.second->verify(this);
+ }
for (const auto &kv : conf_auth_groups) {
const std::string &ag_name = kv.first;
if (ag_name == "default" ||
@@ -1813,6 +1945,12 @@
if (pg.reuse_socket(newp))
return (true);
}
+ for (auto &kv : conf_transport_groups) {
+ struct portal_group &pg = *kv.second;
+
+ if (pg.reuse_socket(newp))
+ return (true);
+ }
return (false);
}
@@ -1864,6 +2002,17 @@
else
newpg.allocate_tag();
}
+ for (auto &kv : conf_transport_groups) {
+ struct portal_group &newpg = *kv.second;
+
+ if (newpg.tag() != 0)
+ continue;
+ auto it = oldconf->conf_transport_groups.find(kv.first);
+ if (it != oldconf->conf_transport_groups.end())
+ newpg.set_tag(it->second->tag());
+ else
+ newpg.allocate_tag();
+ }
/* Deregister on removed iSNS servers. */
for (auto &kv : oldconf->conf_isns) {
@@ -2027,6 +2176,9 @@
for (auto &kv : conf_portal_groups) {
cumulated_error += kv.second->open_sockets(*oldconf);
}
+ for (auto &kv : conf_transport_groups) {
+ cumulated_error += kv.second->open_sockets(*oldconf);
+ }
/*
* Go through the no longer used sockets, closing them.
@@ -2034,6 +2186,9 @@
for (auto &kv : oldconf->conf_portal_groups) {
kv.second->close_sockets();
}
+ for (auto &kv : oldconf->conf_transport_groups) {
+ kv.second->close_sockets();
+ }
/* (Re-)Register on remaining/new iSNS servers. */
for (auto &kv : conf_isns) {
@@ -2397,6 +2552,9 @@
pg = conf->add_portal_group("default");
assert(pg != NULL);
+ pg = conf->add_transport_group("default");
+ assert(pg != NULL);
+
conf_start(conf.get());
if (ucl)
valid = uclparse_conf(path);
@@ -2427,6 +2585,14 @@
pg->add_default_portals();
}
+ if (!conf->default_portal_group_defined()) {
+ log_debugx("transport-group \"default\" not defined; "
+ "going with defaults");
+ pg = conf->find_transport_group("default");
+ assert(pg != NULL);
+ pg->add_default_portals();
+ }
+
if (!conf->verify()) {
conf.reset();
return {};
diff --git a/usr.sbin/ctld/discovery.cc b/usr.sbin/ctld/discovery.cc
--- a/usr.sbin/ctld/discovery.cc
+++ b/usr.sbin/ctld/discovery.cc
@@ -113,6 +113,9 @@
if (pg == nullptr)
continue;
for (const portal_up &portal : pg->portals()) {
+ if (portal->protocol() != portal_protocol::ISCSI &&
+ portal->protocol() != portal_protocol::ISER)
+ continue;
ai = portal->ai();
ret = getnameinfo(ai->ai_addr, ai->ai_addrlen,
hbuf, sizeof(hbuf), sbuf, sizeof(sbuf),
diff --git a/usr.sbin/ctld/kernel.cc b/usr.sbin/ctld/kernel.cc
--- a/usr.sbin/ctld/kernel.cc
+++ b/usr.sbin/ctld/kernel.cc
@@ -126,10 +126,13 @@
std::string port_name;
int pp;
int vp;
+ uint16_t portid;
int cfiscsi_state;
std::string cfiscsi_target;
+ std::string nqn;
uint16_t cfiscsi_portal_group_tag;
std::string ctld_portal_group_name;
+ std::string ctld_transport_group_name;
attr_list attr_list;
};
@@ -322,6 +325,14 @@
cur_port->cfiscsi_portal_group_tag = strtoul(str.c_str(), NULL, 0);
} else if (strcmp(name, "ctld_portal_group_name") == 0) {
cur_port->ctld_portal_group_name = std::move(str);
+ } else if (strcmp(name, "ctld_transport_group_name") == 0) {
+ cur_port->ctld_transport_group_name = std::move(str);
+ } else if (strcmp(name, "nqn") == 0) {
+ cur_port->nqn = std::move(str);
+ } else if (strcmp(name, "portid") == 0) {
+ if (str.empty())
+ log_errx(1, "%s: %s missing its argument", __func__, name);
+ cur_port->portid = strtoul(str.c_str(), NULL, 0);
} else if (strcmp(name, "targ_port") == 0) {
devlist->cur_port = NULL;
} else if (strcmp(name, "ctlportlist") == 0) {
@@ -469,7 +480,7 @@
if (pg == nullptr) {
pg = conf->add_portal_group(pg_name);
if (pg == nullptr) {
- log_warnx("Failed to add portal_group \"%s\"", pg_name);
+ log_warnx("Failed to add portal-group \"%s\"", pg_name);
return;
}
}
@@ -480,6 +491,43 @@
}
}
+void
+add_nvmf_port(struct conf *conf, const struct cctl_port &port,
+ std::string &name)
+{
+ if (port.nqn.empty() || port.ctld_transport_group_name.empty()) {
+ log_debugx("CTL port %u \"%s\" wasn't managed by ctld; ",
+ port.port_id, name.c_str());
+ return;
+ }
+
+ const char *nqn = port.nqn.c_str();
+ struct target *targ = conf->find_controller(nqn);
+ if (targ == nullptr) {
+ targ = conf->add_controller(nqn);
+ if (targ == nullptr) {
+ log_warnx("Failed to add controller \"%s\"", nqn);
+ return;
+ }
+ }
+
+ const char *tg_name = port.ctld_transport_group_name.c_str();
+ struct portal_group *pg = conf->find_transport_group(tg_name);
+ if (pg == nullptr) {
+ pg = conf->add_transport_group(tg_name);
+ if (pg == nullptr) {
+ log_warnx("Failed to add transport-group \"%s\"",
+ tg_name);
+ return;
+ }
+ }
+ pg->set_tag(port.portid);
+ if (!conf->add_port(targ, pg, port.port_id)) {
+ log_warnx("Failed to add port for controller \"%s\" and transport-group \"%s\"",
+ nqn, tg_name);
+ }
+}
+
conf_up
conf_new_from_kernel(struct kports &kports)
{
@@ -505,6 +553,8 @@
if (port.port_frontend == "iscsi") {
add_iscsi_port(kports, conf.get(), port, name);
+ } else if (port.port_frontend == "nvmf") {
+ add_nvmf_port(conf.get(), port, name);
} else {
/* XXX: Treat all unknown ports as iSCSI? */
add_iscsi_port(kports, conf.get(), port, name);
@@ -1060,7 +1110,7 @@
kernel_capsicate(void)
{
cap_rights_t rights;
- const unsigned long cmds[] = { CTL_ISCSI };
+ const unsigned long cmds[] = { CTL_ISCSI, CTL_NVMF };
cap_rights_init(&rights, CAP_IOCTL);
if (caph_rights_limit(ctl_fd, &rights) < 0)
diff --git a/usr.sbin/ctld/nvmf.hh b/usr.sbin/ctld/nvmf.hh
new file mode 100644
--- /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.cc b/usr.sbin/ctld/nvmf.cc
new file mode 100644
--- /dev/null
+++ b/usr.sbin/ctld/nvmf.cc
@@ -0,0 +1,478 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Chelsio Communications, Inc.
+ * Written by: John Baldwin <jhb@FreeBSD.org>
+ */
+
+#include <sys/param.h>
+#include <sys/linker.h>
+#include <sys/module.h>
+#include <sys/time.h>
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <libiscsiutil.h>
+#include <libnvmf.h>
+#include <libutil.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <cam/ctl/ctl.h>
+#include <cam/ctl/ctl_io.h>
+#include <cam/ctl/ctl_ioctl.h>
+
+#include <memory>
+
+#include "ctld.hh"
+#include "nvmf.hh"
+
+#define DEFAULT_MAXH2CDATA (256 * 1024)
+
+struct nvmf_io_portal final : public nvmf_portal {
+ nvmf_io_portal(struct portal_group *pg, const char *listen,
+ portal_protocol protocol, freebsd::addrinfo_up ai,
+ const struct nvmf_association_params &aparams,
+ nvmf_association_up na) :
+ nvmf_portal(pg, listen, protocol, std::move(ai), aparams,
+ std::move(na)) {}
+
+ void handle_connection(freebsd::fd_up fd, const char *host,
+ const struct sockaddr *client_sa) override;
+};
+
+struct nvmf_transport_group final : public portal_group {
+ nvmf_transport_group(struct conf *conf, std::string_view name) :
+ portal_group(conf, name) {}
+
+ const char *keyword() const override
+ { return "transport-group"; }
+
+ void allocate_tag() override;
+ bool add_portal(const char *value, portal_protocol protocol)
+ override;
+ void add_default_portals() override;
+ bool set_filter(const char *str) override;
+
+ virtual port_up create_port(struct target *target, auth_group_sp ag)
+ override;
+ virtual port_up create_port(struct target *target, uint32_t ctl_port)
+ override;
+
+private:
+ struct nvmf_association_params init_aparams(portal_protocol protocol);
+
+ static uint16_t last_port_id;
+};
+
+struct nvmf_port final : public portal_group_port {
+ nvmf_port(struct target *target, struct portal_group *pg,
+ auth_group_sp ag) :
+ portal_group_port(target, pg, ag) {}
+ nvmf_port(struct target *target, struct portal_group *pg,
+ uint32_t ctl_port) :
+ portal_group_port(target, pg, ctl_port) {}
+
+ bool kernel_create_port() override;
+ bool kernel_remove_port() override;
+
+private:
+ static bool modules_loaded;
+ static void load_kernel_modules();
+};
+
+struct nvmf_controller final : public target {
+ nvmf_controller(struct conf *conf, std::string_view name) :
+ target(conf, "controller", name) {}
+
+ bool add_host_nqn(std::string_view name) override;
+ bool add_host_address(const char *addr) override;
+ bool add_namespace(u_int id, const char *lun_name) override;
+ bool add_portal_group(const char *pg_name, const char *ag_name)
+ override;
+ struct lun *start_namespace(u_int id) override;
+
+protected:
+ struct portal_group *default_portal_group() override;
+};
+
+uint16_t nvmf_transport_group::last_port_id = 0;
+bool nvmf_port::modules_loaded = false;
+
+static bool need_tcp_transport = false;
+
+static bool
+parse_bool(const nvlist_t *nvl, const char *key, bool def)
+{
+ const char *value;
+
+ if (!nvlist_exists_string(nvl, key))
+ return def;
+
+ value = nvlist_get_string(nvl, key);
+ if (strcasecmp(value, "true") == 0 ||
+ strcasecmp(value, "1") == 0)
+ return true;
+ if (strcasecmp(value, "false") == 0 ||
+ strcasecmp(value, "0") == 0)
+ return false;
+
+ log_warnx("Invalid value \"%s\" for boolean option %s", value, key);
+ return def;
+}
+
+static uint64_t
+parse_number(const nvlist_t *nvl, const char *key, uint64_t def, uint64_t minv,
+ uint64_t maxv)
+{
+ const char *value;
+ int64_t val;
+
+ if (!nvlist_exists_string(nvl, key))
+ return def;
+
+ value = nvlist_get_string(nvl, key);
+ if (expand_number(value, &val) == 0 && val >= 0 &&
+ (uint64_t)val >= minv && (uint64_t)val <= maxv)
+ return (uint64_t)val;
+
+ log_warnx("Invalid value \"%s\" for numeric option %s", value, key);
+ return def;
+}
+
+struct nvmf_association_params
+nvmf_transport_group::init_aparams(portal_protocol protocol)
+{
+ struct nvmf_association_params params;
+ memset(&params, 0, sizeof(params));
+
+ /* Options shared between discovery and I/O associations. */
+ const nvlist_t *nvl = pg_options.get();
+ params.tcp.header_digests = parse_bool(nvl, "HDGST", false);
+ params.tcp.data_digests = parse_bool(nvl, "DDGST", false);
+ uint64_t value = parse_number(nvl, "MAXH2CDATA", DEFAULT_MAXH2CDATA,
+ 4096, UINT32_MAX);
+ if (value % 4 != 0) {
+ log_warnx("Invalid value \"%ju\" for option MAXH2CDATA",
+ (uintmax_t)value);
+ value = DEFAULT_MAXH2CDATA;
+ }
+ params.tcp.maxh2cdata = value;
+
+ switch (protocol) {
+ case portal_protocol::NVME_TCP:
+ params.sq_flow_control = parse_bool(nvl, "SQFC", false);
+ params.dynamic_controller_model = true;
+ params.max_admin_qsize = parse_number(nvl, "max_admin_qsize",
+ NVME_MAX_ADMIN_ENTRIES, NVME_MIN_ADMIN_ENTRIES,
+ NVME_MAX_ADMIN_ENTRIES);
+ params.max_io_qsize = parse_number(nvl, "max_io_qsize",
+ NVME_MAX_IO_ENTRIES, NVME_MIN_IO_ENTRIES,
+ NVME_MAX_IO_ENTRIES);
+ params.tcp.pda = 0;
+ break;
+ case portal_protocol::NVME_DISCOVERY_TCP:
+ params.sq_flow_control = false;
+ params.dynamic_controller_model = true;
+ params.max_admin_qsize = NVME_MAX_ADMIN_ENTRIES;
+ params.tcp.pda = 0;
+ break;
+ default:
+ __assert_unreachable();
+ }
+
+ return params;
+}
+
+portal_group_up
+nvmf_make_transport_group(struct conf *conf, std::string_view name)
+{
+ return std::make_unique<nvmf_transport_group>(conf, name);
+}
+
+target_up
+nvmf_make_controller(struct conf *conf, std::string_view name)
+{
+ return std::make_unique<nvmf_controller>(conf, name);
+}
+
+void
+nvmf_transport_group::allocate_tag()
+{
+ set_tag(++last_port_id);
+}
+
+bool
+nvmf_transport_group::add_portal(const char *value, portal_protocol protocol)
+{
+ freebsd::addrinfo_up ai;
+ enum nvmf_trtype trtype;
+
+ switch (protocol) {
+ case portal_protocol::NVME_TCP:
+ trtype = NVMF_TRTYPE_TCP;
+ ai = parse_addr_port(value, "4420");
+ break;
+ case portal_protocol::NVME_DISCOVERY_TCP:
+ trtype = NVMF_TRTYPE_TCP;
+ ai = parse_addr_port(value, "8009");
+ break;
+ default:
+ log_warnx("unsupported transport protocol for %s", value);
+ return false;
+ }
+
+ if (!ai) {
+ log_warnx("invalid listen address %s", value);
+ return false;
+ }
+
+ struct nvmf_association_params aparams = init_aparams(protocol);
+ nvmf_association_up association(nvmf_allocate_association(trtype, true,
+ &aparams));
+ if (!association) {
+ log_warn("Failed to create NVMe controller association");
+ return false;
+ }
+
+ /*
+ * XXX: getaddrinfo(3) may return multiple addresses; we should turn
+ * those into multiple portals.
+ */
+
+ portal_up portal;
+ if (protocol == portal_protocol::NVME_DISCOVERY_TCP) {
+ portal = std::make_unique<nvmf_discovery_portal>(this, value,
+ protocol, std::move(ai), aparams, std::move(association));
+ } else {
+ portal = std::make_unique<nvmf_io_portal>(this, value,
+ protocol, std::move(ai), aparams, std::move(association));
+ need_tcp_transport = true;
+ }
+
+ pg_portals.emplace_back(std::move(portal));
+ return true;
+}
+
+void
+nvmf_transport_group::add_default_portals()
+{
+ add_portal("0.0.0.0", portal_protocol::NVME_DISCOVERY_TCP);
+ add_portal("[::]", portal_protocol::NVME_DISCOVERY_TCP);
+ add_portal("0.0.0.0", portal_protocol::NVME_TCP);
+ add_portal("[::]", portal_protocol::NVME_TCP);
+}
+
+bool
+nvmf_transport_group::set_filter(const char *str)
+{
+ enum discovery_filter filter;
+
+ if (strcmp(str, "none") == 0) {
+ filter = discovery_filter::NONE;
+ } else if (strcmp(str, "address") == 0) {
+ filter = discovery_filter::PORTAL;
+ } else if (strcmp(str, "address-name") == 0) {
+ filter = discovery_filter::PORTAL_NAME;
+ } else {
+ log_warnx("invalid discovery-filter \"%s\" for transport-group "
+ "\"%s\"; valid values are \"none\", \"address\", "
+ "and \"address-name\"",
+ str, name());
+ return false;
+ }
+
+ if (pg_discovery_filter != discovery_filter::UNKNOWN &&
+ pg_discovery_filter != filter) {
+ log_warnx("cannot set discovery-filter to \"%s\" for "
+ "transport-group \"%s\"; already has a different "
+ "value", str, name());
+ return false;
+ }
+
+ pg_discovery_filter = filter;
+ return true;
+}
+
+port_up
+nvmf_transport_group::create_port(struct target *target, auth_group_sp ag)
+{
+ return std::make_unique<nvmf_port>(target, this, ag);
+}
+
+port_up
+nvmf_transport_group::create_port(struct target *target, uint32_t ctl_port)
+{
+ return std::make_unique<nvmf_port>(target, this, ctl_port);
+}
+
+void
+nvmf_port::load_kernel_modules()
+{
+ int saved_errno;
+
+ if (modules_loaded)
+ return;
+
+ saved_errno = errno;
+ if (modfind("nvmft") == -1 && kldload("nvmft") == -1)
+ log_warn("couldn't load nvmft");
+
+ if (need_tcp_transport) {
+ if (modfind("nvmf/tcp") == -1 && kldload("nvmf_tcp") == -1)
+ log_warn("couldn't load nvmf_tcp");
+ }
+
+ errno = saved_errno;
+ modules_loaded = true;
+}
+
+bool
+nvmf_port::kernel_create_port()
+{
+ struct portal_group *pg = p_portal_group;
+ struct target *targ = p_target;
+
+ load_kernel_modules();
+
+ freebsd::nvlist_up nvl = pg->options();
+ nvlist_add_string(nvl.get(), "subnqn", targ->name());
+ nvlist_add_string(nvl.get(), "ctld_transport_group_name",
+ pg->name());
+ nvlist_add_stringf(nvl.get(), "portid", "%u", pg->tag());
+ if (!nvlist_exists_string(nvl.get(), "max_io_qsize"))
+ nvlist_add_stringf(nvl.get(), "max_io_qsize", "%u",
+ NVME_MAX_IO_ENTRIES);
+
+ return ctl_create_port("nvmf", nvl.get(), &p_ctl_port);
+}
+
+bool
+nvmf_port::kernel_remove_port()
+{
+ freebsd::nvlist_up nvl(nvlist_create(0));
+ nvlist_add_string(nvl.get(), "subnqn", p_target->name());
+
+ return ctl_remove_port("nvmf", nvl.get());
+}
+
+bool
+nvmf_controller::add_host_nqn(std::string_view name)
+{
+ if (!use_private_auth("host-nqn"))
+ return false;
+ return t_auth_group->add_host_nqn(name);
+}
+
+bool
+nvmf_controller::add_host_address(const char *addr)
+{
+ if (!use_private_auth("host-address"))
+ return false;
+ return t_auth_group->add_host_address(addr);
+}
+
+bool
+nvmf_controller::add_namespace(u_int id, const char *lun_name)
+{
+ if (id == 0) {
+ log_warnx("namespace ID cannot be 0 for %s", label());
+ return false;
+ }
+
+ std::string lun_label = "namespace ID " + std::to_string(id - 1);
+ return target::add_lun(id, lun_label.c_str(), lun_name);
+}
+
+bool
+nvmf_controller::add_portal_group(const char *pg_name, const char *ag_name)
+{
+ struct portal_group *pg;
+ auth_group_sp ag;
+
+ pg = t_conf->find_transport_group(pg_name);
+ if (pg == NULL) {
+ log_warnx("unknown transport-group \"%s\" for %s", pg_name,
+ label());
+ return false;
+ }
+
+ if (ag_name != NULL) {
+ ag = t_conf->find_auth_group(ag_name);
+ if (ag == NULL) {
+ log_warnx("unknown auth-group \"%s\" for %s", ag_name,
+ label());
+ return false;
+ }
+ }
+
+ if (!t_conf->add_port(this, pg, std::move(ag))) {
+ log_warnx("can't link transport-group \"%s\" to %s", pg_name,
+ label());
+ return false;
+ }
+ return true;
+}
+
+struct lun *
+nvmf_controller::start_namespace(u_int id)
+{
+ if (id == 0) {
+ log_warnx("namespace ID cannot be 0 for %s", label());
+ return nullptr;
+ }
+
+ std::string lun_label = "namespace ID " + std::to_string(id - 1);
+ std::string lun_name = freebsd::stringf("%s,nsid,%u", name(), id);
+ return target::start_lun(id, lun_label.c_str(), lun_name.c_str());
+}
+
+struct portal_group *
+nvmf_controller::default_portal_group()
+{
+ return t_conf->find_transport_group("default");
+}
+
+void
+nvmf_io_portal::handle_connection(freebsd::fd_up fd, const char *host __unused,
+ const struct sockaddr *client_sa __unused)
+{
+ struct nvmf_qpair_params qparams;
+ memset(&qparams, 0, sizeof(qparams));
+ qparams.tcp.fd = fd;
+
+ struct nvmf_capsule *nc = NULL;
+ struct nvmf_fabric_connect_data data;
+ nvmf_qpair_up qp(nvmf_accept(association(), &qparams, &nc, &data));
+ if (!qp) {
+ log_warnx("Failed to create NVMe I/O qpair: %s",
+ nvmf_association_error(association()));
+ return;
+ }
+ nvmf_capsule_up nc_guard(nc);
+ const struct nvmf_fabric_connect_cmd *cmd =
+ (const struct nvmf_fabric_connect_cmd *)nvmf_capsule_sqe(nc);
+
+ struct ctl_nvmf req;
+ memset(&req, 0, sizeof(req));
+ req.type = CTL_NVMF_HANDOFF;
+ int error = nvmf_handoff_controller_qpair(qp.get(), cmd, &data,
+ &req.data.handoff);
+ if (error != 0) {
+ log_warnc(error,
+ "Failed to prepare NVMe I/O qpair for handoff");
+ return;
+ }
+
+ if (ioctl(ctl_fd, CTL_NVMF, &req) != 0)
+ log_warn("ioctl(CTL_NVMF/CTL_NVMF_HANDOFF)");
+ if (req.status == CTL_NVMF_ERROR)
+ log_warnx("Failed to handoff NVMF connection: %s",
+ req.error_str);
+ else if (req.status != CTL_NVMF_OK)
+ log_warnx("Failed to handoff NVMF connection with status %d",
+ req.status);
+}
diff --git a/usr.sbin/ctld/nvmf_discovery.cc b/usr.sbin/ctld/nvmf_discovery.cc
new file mode 100644
--- /dev/null
+++ b/usr.sbin/ctld/nvmf_discovery.cc
@@ -0,0 +1,518 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2023-2025 Chelsio Communications, Inc.
+ * Written by: John Baldwin <jhb@FreeBSD.org>
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <netdb.h>
+#include <libiscsiutil.h>
+#include <libnvmf.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <netinet/in.h>
+
+#include "ctld.hh"
+#include "nvmf.hh"
+
+struct discovery_log {
+ discovery_log(const struct portal_group *pg);
+
+ const char *data() const { return buf.data(); }
+ size_t length() const { return buf.size(); }
+
+ void append(const struct nvme_discovery_log_entry *entry);
+
+private:
+ struct nvme_discovery_log *header()
+ { return reinterpret_cast<struct nvme_discovery_log *>(buf.data()); }
+
+ std::vector<char> buf;
+};
+
+struct discovery_controller {
+ discovery_controller(freebsd::fd_up s, struct nvmf_qpair *qp,
+ const discovery_log &discovery_log);
+
+ void handle_admin_commands();
+private:
+ bool update_cc(uint32_t new_cc);
+ void handle_property_get(const struct nvmf_capsule *nc,
+ const struct nvmf_fabric_prop_get_cmd *pget);
+ void handle_property_set(const struct nvmf_capsule *nc,
+ const struct nvmf_fabric_prop_set_cmd *pset);
+ void handle_fabrics_command(const struct nvmf_capsule *nc,
+ const struct nvmf_fabric_cmd *cmd);
+ void handle_identify_command(const struct nvmf_capsule *nc,
+ const struct nvme_command *cmd);
+ void handle_get_log_page_command(const struct nvmf_capsule *nc,
+ const struct nvme_command *cmd);
+
+ struct nvmf_qpair *qp;
+
+ uint64_t cap = 0;
+ uint32_t vs = 0;
+ uint32_t cc = 0;
+ uint32_t csts = 0;
+
+ bool shutdown = false;
+
+ struct nvme_controller_data cdata;
+
+ const discovery_log &discovery_log;
+ freebsd::fd_up s;
+};
+
+discovery_log::discovery_log(const struct portal_group *pg) :
+ buf(sizeof(nvme_discovery_log))
+{
+ struct nvme_discovery_log *log = header();
+
+ log->genctr = htole32(pg->conf()->genctr());
+ log->recfmt = 0;
+}
+
+void
+discovery_log::append(const struct nvme_discovery_log_entry *entry)
+{
+ const char *cp = reinterpret_cast<const char *>(entry);
+ buf.insert(buf.end(), cp, cp + sizeof(*entry));
+
+ struct nvme_discovery_log *log = header();
+ log->numrec = htole32(le32toh(log->numrec) + 1);
+}
+
+static bool
+discovery_controller_filtered(const struct portal_group *pg,
+ const struct sockaddr *client_sa, std::string_view hostnqn,
+ const struct port *port)
+{
+ const struct target *targ = port->target();
+ const struct auth_group *ag = port->auth_group();
+ if (ag == nullptr)
+ ag = targ->auth_group();
+
+ assert(pg->discovery_filter() != discovery_filter::UNKNOWN);
+
+ if (pg->discovery_filter() >= discovery_filter::PORTAL &&
+ !ag->host_permitted(client_sa)) {
+ log_debugx("host address does not match addresses "
+ "allowed for controller \"%s\"; skipping", targ->name());
+ return true;
+ }
+
+ if (pg->discovery_filter() >= discovery_filter::PORTAL_NAME &&
+ !ag->host_permitted(hostnqn) != 0) {
+ log_debugx("HostNQN does not match NQNs "
+ "allowed for controller \"%s\"; skipping", targ->name());
+ return true;
+ }
+
+ /* XXX: auth not yet implemented for NVMe */
+
+ return false;
+}
+
+static bool
+portal_uses_wildcard_address(const struct portal *p)
+{
+ const struct addrinfo *ai = p->ai();
+
+ switch (ai->ai_family) {
+ case AF_INET:
+ {
+ const struct sockaddr_in *sin;
+
+ sin = (const struct sockaddr_in *)ai->ai_addr;
+ return sin->sin_addr.s_addr == htonl(INADDR_ANY);
+ }
+ case AF_INET6:
+ {
+ const struct sockaddr_in6 *sin6;
+
+ sin6 = (const struct sockaddr_in6 *)ai->ai_addr;
+ return memcmp(&sin6->sin6_addr, &in6addr_any,
+ sizeof(in6addr_any)) == 0;
+ }
+ default:
+ __assert_unreachable();
+ }
+}
+
+static bool
+init_discovery_log_entry(struct nvme_discovery_log_entry *entry,
+ const struct target *target, const struct portal *portal,
+ const char *wildcard_host)
+{
+ /*
+ * The TCP port for I/O controllers might not be fixed, so
+ * fetch the sockaddr of the socket to determine which port
+ * the kernel chose.
+ */
+ struct sockaddr_storage ss;
+ socklen_t len = sizeof(ss);
+ if (getsockname(portal->socket(), (struct sockaddr *)&ss, &len) == -1) {
+ log_warn("Failed getsockname building discovery log entry");
+ return false;
+ }
+
+ const struct nvmf_association_params *aparams =
+ static_cast<const nvmf_portal *>(portal)->aparams();
+
+ memset(entry, 0, sizeof(*entry));
+ entry->trtype = NVMF_TRTYPE_TCP;
+ int error = getnameinfo((struct sockaddr *)&ss, len,
+ (char *)entry->traddr, sizeof(entry->traddr),
+ (char *)entry->trsvcid, sizeof(entry->trsvcid),
+ NI_NUMERICHOST | NI_NUMERICSERV);
+ if (error != 0) {
+ log_warnx("Failed getnameinfo building discovery log entry: %s",
+ gai_strerror(error));
+ return false;
+ }
+
+ if (portal_uses_wildcard_address(portal))
+ strncpy((char *)entry->traddr, wildcard_host,
+ sizeof(entry->traddr));
+ switch (portal->ai()->ai_family) {
+ case AF_INET:
+ entry->adrfam = NVMF_ADRFAM_IPV4;
+ break;
+ case AF_INET6:
+ entry->adrfam = NVMF_ADRFAM_IPV6;
+ break;
+ default:
+ __assert_unreachable();
+ }
+ entry->subtype = NVMF_SUBTYPE_NVME;
+ if (!aparams->sq_flow_control)
+ entry->treq |= (1 << 2);
+ entry->portid = htole16(portal->portal_group()->tag());
+ entry->cntlid = htole16(NVMF_CNTLID_DYNAMIC);
+ entry->aqsz = aparams->max_admin_qsize;
+ strncpy((char *)entry->subnqn, target->name(), sizeof(entry->subnqn));
+ return true;
+}
+
+static discovery_log
+build_discovery_log_page(const struct portal_group *pg, int fd,
+ const struct sockaddr *client_sa,
+ const struct nvmf_fabric_connect_data &data)
+{
+ discovery_log discovery_log(pg);
+
+ struct sockaddr_storage ss;
+ socklen_t len = sizeof(ss);
+ if (getsockname(fd, (struct sockaddr *)&ss, &len) == -1) {
+ log_warn("build_discovery_log_page: getsockname");
+ return discovery_log;
+ }
+
+ char wildcard_host[NI_MAXHOST];
+ int error = getnameinfo((struct sockaddr *)&ss, len, wildcard_host,
+ sizeof(wildcard_host), NULL, 0, NI_NUMERICHOST);
+ if (error != 0) {
+ log_warnx("build_discovery_log_page: getnameinfo: %s",
+ gai_strerror(error));
+ return discovery_log;
+ }
+
+ const char *nqn = (const char *)data.hostnqn;
+ std::string hostnqn(nqn, strnlen(nqn, sizeof(data.hostnqn)));
+ for (const auto &kv : pg->ports()) {
+ const struct port *port = kv.second;
+ if (discovery_controller_filtered(pg, client_sa, hostnqn, port))
+ continue;
+
+ for (const portal_up &portal : pg->portals()) {
+ if (portal->protocol() != portal_protocol::NVME_TCP)
+ continue;
+
+ if (portal_uses_wildcard_address(portal.get()) &&
+ portal->ai()->ai_family != client_sa->sa_family)
+ continue;
+
+ struct nvme_discovery_log_entry entry;
+ if (init_discovery_log_entry(&entry, port->target(),
+ portal.get(), wildcard_host))
+ discovery_log.append(&entry);
+ }
+ }
+
+ return discovery_log;
+}
+
+bool
+discovery_controller::update_cc(uint32_t new_cc)
+{
+ uint32_t changes;
+
+ if (shutdown)
+ return false;
+ if (!nvmf_validate_cc(qp, cap, cc, new_cc))
+ return false;
+
+ changes = cc ^ new_cc;
+ cc = new_cc;
+
+ /* Handle shutdown requests. */
+ if (NVMEV(NVME_CC_REG_SHN, changes) != 0 &&
+ NVMEV(NVME_CC_REG_SHN, new_cc) != 0) {
+ csts &= ~NVMEM(NVME_CSTS_REG_SHST);
+ csts |= NVMEF(NVME_CSTS_REG_SHST, NVME_SHST_COMPLETE);
+ shutdown = true;
+ }
+
+ if (NVMEV(NVME_CC_REG_EN, changes) != 0) {
+ if (NVMEV(NVME_CC_REG_EN, new_cc) == 0) {
+ /* Controller reset. */
+ csts = 0;
+ shutdown = true;
+ } else
+ csts |= NVMEF(NVME_CSTS_REG_RDY, 1);
+ }
+ return true;
+}
+
+void
+discovery_controller::handle_property_get(const struct nvmf_capsule *nc,
+ const struct nvmf_fabric_prop_get_cmd *pget)
+{
+ struct nvmf_fabric_prop_get_rsp rsp;
+
+ nvmf_init_cqe(&rsp, nc, 0);
+
+ switch (le32toh(pget->ofst)) {
+ case NVMF_PROP_CAP:
+ if (pget->attrib.size != NVMF_PROP_SIZE_8)
+ goto error;
+ rsp.value.u64 = htole64(cap);
+ break;
+ case NVMF_PROP_VS:
+ if (pget->attrib.size != NVMF_PROP_SIZE_4)
+ goto error;
+ rsp.value.u32.low = htole32(vs);
+ break;
+ case NVMF_PROP_CC:
+ if (pget->attrib.size != NVMF_PROP_SIZE_4)
+ goto error;
+ rsp.value.u32.low = htole32(cc);
+ break;
+ case NVMF_PROP_CSTS:
+ if (pget->attrib.size != NVMF_PROP_SIZE_4)
+ goto error;
+ rsp.value.u32.low = htole32(csts);
+ break;
+ default:
+ goto error;
+ }
+
+ nvmf_send_response(nc, &rsp);
+ return;
+error:
+ nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD);
+}
+
+void
+discovery_controller::handle_property_set(const struct nvmf_capsule *nc,
+ const struct nvmf_fabric_prop_set_cmd *pset)
+{
+ switch (le32toh(pset->ofst)) {
+ case NVMF_PROP_CC:
+ if (pset->attrib.size != NVMF_PROP_SIZE_4)
+ goto error;
+ if (!update_cc(le32toh(pset->value.u32.low)))
+ goto error;
+ break;
+ default:
+ goto error;
+ }
+
+ nvmf_send_success(nc);
+ return;
+error:
+ nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD);
+}
+
+void
+discovery_controller::handle_fabrics_command(const struct nvmf_capsule *nc,
+ const struct nvmf_fabric_cmd *fc)
+{
+ switch (fc->fctype) {
+ case NVMF_FABRIC_COMMAND_PROPERTY_GET:
+ handle_property_get(nc,
+ (const struct nvmf_fabric_prop_get_cmd *)fc);
+ break;
+ case NVMF_FABRIC_COMMAND_PROPERTY_SET:
+ handle_property_set(nc,
+ (const struct nvmf_fabric_prop_set_cmd *)fc);
+ break;
+ case NVMF_FABRIC_COMMAND_CONNECT:
+ log_warnx("CONNECT command on connected queue");
+ nvmf_send_generic_error(nc, NVME_SC_COMMAND_SEQUENCE_ERROR);
+ break;
+ case NVMF_FABRIC_COMMAND_DISCONNECT:
+ log_warnx("DISCONNECT command on admin queue");
+ nvmf_send_error(nc, NVME_SCT_COMMAND_SPECIFIC,
+ NVMF_FABRIC_SC_INVALID_QUEUE_TYPE);
+ break;
+ default:
+ log_warnx("Unsupported fabrics command %#x", fc->fctype);
+ nvmf_send_generic_error(nc, NVME_SC_INVALID_OPCODE);
+ break;
+ }
+}
+
+void
+discovery_controller::handle_identify_command(const struct nvmf_capsule *nc,
+ const struct nvme_command *cmd)
+{
+ uint8_t cns;
+
+ cns = le32toh(cmd->cdw10) & 0xFF;
+ switch (cns) {
+ case 1:
+ break;
+ default:
+ log_warnx("Unsupported CNS %#x for IDENTIFY", cns);
+ goto error;
+ }
+
+ nvmf_send_controller_data(nc, &cdata, sizeof(cdata));
+ return;
+error:
+ nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD);
+}
+
+void
+discovery_controller::handle_get_log_page_command(const struct nvmf_capsule *nc,
+ const struct nvme_command *cmd)
+{
+ uint64_t offset;
+ uint32_t length;
+
+ switch (nvmf_get_log_page_id(cmd)) {
+ case NVME_LOG_DISCOVERY:
+ break;
+ default:
+ log_warnx("Unsupported log page %u for discovery controller",
+ nvmf_get_log_page_id(cmd));
+ goto error;
+ }
+
+ offset = nvmf_get_log_page_offset(cmd);
+ if (offset >= discovery_log.length())
+ goto error;
+
+ length = nvmf_get_log_page_length(cmd);
+ if (length > discovery_log.length() - offset)
+ length = discovery_log.length() - offset;
+
+ nvmf_send_controller_data(nc, discovery_log.data() + offset, length);
+ return;
+error:
+ nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD);
+}
+
+void
+discovery_controller::handle_admin_commands()
+{
+ for (;;) {
+ struct nvmf_capsule *nc;
+ int error = nvmf_controller_receive_capsule(qp, &nc);
+ if (error != 0) {
+ if (error != ECONNRESET)
+ log_warnc(error,
+ "Failed to read command capsule");
+ break;
+ }
+ nvmf_capsule_up nc_guard(nc);
+
+ const struct nvme_command *cmd =
+ (const struct nvme_command *)nvmf_capsule_sqe(nc);
+
+ /*
+ * Only permit Fabrics commands while a controller is
+ * disabled.
+ */
+ if (NVMEV(NVME_CC_REG_EN, cc) == 0 &&
+ cmd->opc != NVME_OPC_FABRICS_COMMANDS) {
+ log_warnx("Unsupported admin opcode %#x while disabled\n",
+ cmd->opc);
+ nvmf_send_generic_error(nc,
+ NVME_SC_COMMAND_SEQUENCE_ERROR);
+ continue;
+ }
+
+ switch (cmd->opc) {
+ case NVME_OPC_FABRICS_COMMANDS:
+ handle_fabrics_command(nc,
+ (const struct nvmf_fabric_cmd *)cmd);
+ break;
+ case NVME_OPC_IDENTIFY:
+ handle_identify_command(nc, cmd);
+ break;
+ case NVME_OPC_GET_LOG_PAGE:
+ handle_get_log_page_command(nc, cmd);
+ break;
+ default:
+ log_warnx("Unsupported admin opcode %#x", cmd->opc);
+ nvmf_send_generic_error(nc, NVME_SC_INVALID_OPCODE);
+ break;
+ }
+ }
+}
+
+discovery_controller::discovery_controller(freebsd::fd_up fd,
+ struct nvmf_qpair *qp, const struct discovery_log &discovery_log) :
+ qp(qp), discovery_log(discovery_log), s(std::move(fd))
+{
+ nvmf_init_discovery_controller_data(qp, &cdata);
+ cap = nvmf_controller_cap(qp);
+ vs = cdata.ver;
+}
+
+void
+nvmf_discovery_portal::handle_connection(freebsd::fd_up fd,
+ const char *host __unused, const struct sockaddr *client_sa)
+{
+ struct nvmf_qpair_params qparams;
+ memset(&qparams, 0, sizeof(qparams));
+ qparams.tcp.fd = fd;
+
+ struct nvmf_capsule *nc = NULL;
+ struct nvmf_fabric_connect_data data;
+ nvmf_qpair_up qp(nvmf_accept(association(), &qparams, &nc, &data));
+ if (!qp) {
+ log_warnx("Failed to create NVMe discovery qpair: %s",
+ nvmf_association_error(association()));
+ return;
+ }
+ nvmf_capsule_up nc_guard(nc);
+
+ if (strncmp((char *)data.subnqn, NVMF_DISCOVERY_NQN,
+ sizeof(data.subnqn)) != 0) {
+ log_warnx("Discovery NVMe qpair with invalid SubNQN: %.*s",
+ (int)sizeof(data.subnqn), data.subnqn);
+ nvmf_connect_invalid_parameters(nc, true,
+ offsetof(struct nvmf_fabric_connect_data, subnqn));
+ return;
+ }
+
+ /* Just use a controller ID of 1 for all discovery controllers. */
+ int error = nvmf_finish_accept(nc, 1);
+ if (error != 0) {
+ log_warnc(error, "Failed to send NVMe CONNECT reponse");
+ return;
+ }
+ nc_guard.reset();
+
+ discovery_log discovery_log = build_discovery_log_page(portal_group(),
+ fd, client_sa, data);
+
+ discovery_controller controller(std::move(fd), qp.get(), discovery_log);
+ controller.handle_admin_commands();
+}
diff --git a/usr.sbin/ctld/parse.y b/usr.sbin/ctld/parse.y
--- a/usr.sbin/ctld/parse.y
+++ b/usr.sbin/ctld/parse.y
@@ -54,12 +54,14 @@
%}
%token ALIAS AUTH_GROUP AUTH_TYPE BACKEND BLOCKSIZE CHAP CHAP_MUTUAL
-%token CLOSING_BRACKET CTL_LUN DEBUG DEVICE_ID DEVICE_TYPE
-%token DISCOVERY_AUTH_GROUP DISCOVERY_FILTER DSCP FOREIGN
+%token CLOSING_BRACKET CONTROLLER CTL_LUN DEBUG DEVICE_ID DEVICE_TYPE
+%token DISCOVERY_AUTH_GROUP DISCOVERY_FILTER DISCOVERY_TCP DSCP FOREIGN
+%token HOST_ADDRESS HOST_NQN
%token INITIATOR_NAME INITIATOR_PORTAL ISNS_SERVER ISNS_PERIOD ISNS_TIMEOUT
-%token LISTEN LISTEN_ISER LUN MAXPROC OFFLOAD OPENING_BRACKET OPTION
+%token LISTEN LISTEN_ISER LUN MAXPROC NAMESPACE
+%token OFFLOAD OPENING_BRACKET OPTION
%token PATH PCP PIDFILE PORT PORTAL_GROUP REDIRECT SEMICOLON SERIAL
-%token SIZE STR TAG TARGET TIMEOUT
+%token SIZE STR TAG TARGET TCP TIMEOUT TRANSPORT_GROUP
%token AF11 AF12 AF13 AF21 AF22 AF23 AF31 AF32 AF33 AF41 AF42 AF43
%token BE EF CS0 CS1 CS2 CS3 CS4 CS5 CS6 CS7
@@ -98,9 +100,13 @@
|
portal_group
|
+ transport_group
+ |
lun
|
target
+ |
+ controller
;
debug: DEBUG STR
@@ -232,6 +238,10 @@
|
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_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 @@
}
;
+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 @@
}
;
+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
--- a/usr.sbin/ctld/token.l
+++ b/usr.sbin/ctld/token.l
@@ -54,21 +54,26 @@
blocksize { return BLOCKSIZE; }
chap { return CHAP; }
chap-mutual { return CHAP_MUTUAL; }
+controller { return CONTROLLER; }
ctl-lun { return CTL_LUN; }
debug { return DEBUG; }
device-id { return DEVICE_ID; }
device-type { return DEVICE_TYPE; }
discovery-auth-group { return DISCOVERY_AUTH_GROUP; }
discovery-filter { return DISCOVERY_FILTER; }
+discovery-tcp { return DISCOVERY_TCP; }
dscp { return DSCP; }
pcp { return PCP; }
foreign { return FOREIGN; }
+host-address { return HOST_ADDRESS; }
+host-nqn { return HOST_NQN; }
initiator-name { return INITIATOR_NAME; }
initiator-portal { return INITIATOR_PORTAL; }
listen { return LISTEN; }
listen-iser { return LISTEN_ISER; }
lun { return LUN; }
maxproc { return MAXPROC; }
+namespace { return NAMESPACE; }
offload { return OFFLOAD; }
option { return OPTION; }
path { return PATH; }
@@ -83,7 +88,9 @@
size { return SIZE; }
tag { return TAG; }
target { return TARGET; }
+tcp { return TCP; }
timeout { return TIMEOUT; }
+transport-group { return TRANSPORT_GROUP; }
af11 { return AF11; }
af12 { return AF12; }
af13 { return AF13; }
diff --git a/usr.sbin/ctld/uclparse.cc b/usr.sbin/ctld/uclparse.cc
--- a/usr.sbin/ctld/uclparse.cc
+++ b/usr.sbin/ctld/uclparse.cc
@@ -64,6 +64,10 @@
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 &);
@@ -229,6 +233,47 @@
NULL));
}
+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)
{
@@ -284,6 +329,62 @@
return (target_add_lun(id, name.string_value().c_str()));
}
+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)
{
@@ -390,6 +491,19 @@
}
}
+ 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 @@
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 @@
}
}
+ 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(
@@ -738,6 +903,222 @@
return (true);
}
+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)
{

File Metadata

Mime Type
text/plain
Expires
Wed, May 20, 10:32 AM (15 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33344425
Default Alt Text
D48773.diff (73 KB)

Event Timeline