Loading CMakeLists.txt +5 −2 Changes for CMakeLists.txt: 5 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -2,7 +2,8 @@ cmake_minimum_required(VERSION 3.16) project(sharedd VERSION 0.1.0 LANGUAGES CXX) # sharedd: SMB and NFS shares served by the kernel's own file servers -- # ksmbd for SMB2/3 and knfsd for NFSv4. sharedd is what the kernel asks # ksmbd for SMB2/3 and knfsd for NFSv4 -- and iSCSI targets (LVM volumes, # disks, image files) served by the kernel's LIO, set up over configfs. sharedd is what the kernel asks # who may log in and which share means which directory (in place of # ksmbd.mountd and rpc.mountd), keeps the shares in /etc/sharedd/sharedd.yaml # (libconfplus) and offers them on sysbus as the service "sharedd". Loading Loading @@ -34,6 +35,7 @@ add_executable(sharedd src/nfs.cpp src/rpc.cpp src/files.cpp src/iscsi.cpp ) target_include_directories(sharedd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src) target_compile_definitions(sharedd PRIVATE SHAREDD_CONFIG="${SHAREDD_CONFIG}") Loading @@ -45,6 +47,7 @@ target_link_libraries(sharectl PRIVATE sysbus::sysbus) install(TARGETS sharedd sharectl RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR}) install(DIRECTORY include/sharedd DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(FILES dist/sharedd.yaml DESTINATION /etc/sharedd) # iSCSI CHAP secrets: root only install(FILES dist/sharedd.yaml DESTINATION /etc/sharedd PERMISSIONS OWNER_READ OWNER_WRITE) set(SHAREDD_UNITDIR "/etc/syslite/services" CACHE PATH "syslite unit directory") install(FILES dist/sharedd.service DESTINATION ${SHAREDD_UNITDIR}) dist/sharedd.yaml +11 −0 Changes for dist/sharedd.yaml: 11 added lines, 0 removed lines. Original line number Diff line number Diff line # sharedd: SMB and NFS shares (served by the kernel's ksmbd and knfsd). # Edit with sharectl or webctl; by hand, then: sharectl reload # Keeps iSCSI CHAP secrets: mode 0600. sharedd: smb: enabled: true Loading @@ -23,3 +24,13 @@ sharedd: # nfs: true # nfs_clients: ["192.168.1.0/24"] # squash: root # root|all|none iscsi: enabled: true # LIO, the kernel's iSCSI target (port 3260) portals: ["0.0.0.0:3260"] # base_name: iqn.2026-01.de.tuxist.fenriros:<host> # made up on the first start # targets: # - name: vm1 # the target is <base_name>:vm1 # luns: [/dev/vg0/vm1] # LVM volumes, disks, md arrays or image files # initiators: [iqn.2005-03.org.open-iscsi:1234abcd] # chap_user: vm1 # optional; mutual: chap_user_in, chap_password_in # chap_password: secret12345678 include/sharedd/api.h +34 −3 Changes for include/sharedd/api.h: 34 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -52,10 +52,32 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * min_protocol, max_protocol (SMB2_10 SMB3_00 SMB3_02 SMB3_11), signing * (disabled|enabled|auto|mandatory), map_to_guest (never|bad_user), * guest_account, nfs (yes|no), nfs_threads, nfs_root (the directory NFSv4 * clients see as "/"; NFS shares have to be below it). * clients see as "/"; NFS shares have to be below it), iscsi (yes|no), * iscsi_base_name (targets are <base>:<name>), iscsi_portal (repeated: * address:port or [IPv6]:port to listen on). * * Status: smb_state / nfs_state (disabled|running|failed), smb_error / * nfs_error, shares. */ * Status: smb_state / nfs_state / iscsi_state (disabled|running|failed), * smb_error / nfs_error / iscsi_error, shares, iscsi_targets. * * An iSCSI target (served by the kernel's LIO), in replies and as * arguments: * name [a-z0-9.-] (max. 63); the target is <base name>:<name> * iqn replies only * comment free text * lun repeated, LUN 0, 1, ...: a block device (LVM volume, * md array, disk, partition) or an image file (>= 1 MiB) * read_only yes|no * open yes|no: any initiator may connect (no ACL list) * initiator repeated: the initiators' IQNs allowed in * chap_user, chap_password CHAP (empty: none); replies have * chap_password_set instead * chap_user_in, chap_password_in mutual CHAP: what the target answers * with; replies: chap_password_in_set * ListIscsiTargets repeats these per target, each starting with its * "name", plus "session" (repeated: initiators logged in) and "error" (why * it couldn't be set up, e.g. the volume is mounted here). EditIscsiTarget * takes name + the whole new target (rename: new_name); a missing * chap_password/chap_password_in keeps the stored one. */ #pragma once Loading @@ -74,6 +96,15 @@ constexpr const char *GetSettings = "GetSettings"; constexpr const char *SetSettings = "SetSettings"; // the settings to change constexpr const char *Reload = "Reload"; // re-read the configuration file // iSCSI targets (changes: root only) constexpr const char *ListIscsiTargets = "ListIscsiTargets"; constexpr const char *CreateIscsiTarget = "CreateIscsiTarget"; // a target constexpr const char *EditIscsiTarget = "EditIscsiTarget"; // name + the whole new target constexpr const char *DeleteIscsiTarget = "DeleteIscsiTarget"; // name // block devices that could be LUNs -> per device: device (path), kind // (lvm|md|disk|partition), size (bytes), in_use (why not, "" if free) constexpr const char *ListIscsiDevices = "ListIscsiDevices"; // Files and folders inside a share (root only). path: inside the share, // "" for its own folder; ".." and symlinks can't lead out of it. // Browse {share, path} -> path, self_owner/self_group/self_mode (the Loading src/config.cpp +184 −1 Changes for src/config.cpp: 184 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -191,6 +191,140 @@ Share *Config::find(const std::string &name) { return nullptr; } IscsiTarget *Config::findIscsi(const std::string &name) { for (IscsiTarget &t : iscsiTargets) if (t.name == lower(name)) return &t; return nullptr; } bool IscsiTarget::operator==(const IscsiTarget &o) const { return name == o.name && comment == o.comment && luns == o.luns && readOnly == o.readOnly && initiators == o.initiators && open == o.open && chapUser == o.chapUser && chapPassword == o.chapPassword && chapUserIn == o.chapUserIn && chapPasswordIn == o.chapPasswordIn; } std::string defaultIscsiBaseName() { char host[256] = ""; gethostname(host, sizeof(host) - 1); std::string h; for (char c : std::string(host).substr(0, std::string(host).find('.'))) if (isalnum((unsigned char)c) || c == '-') h += (char)tolower((unsigned char)c); return "iqn.2026-01.de.tuxist.fenriros:" + (h.empty() ? std::string("fenriros") : h); } bool validIscsiName(std::string &name) { name = lower(name); if (name.size() < 5 || name.size() > 223) return false; std::string type = name.substr(0, 4); if (type == "iqn.") { // iqn.YYYY-MM.reversed.domain[:anything] if (name.size() < 12 || name[8] != '-' || name[11] != '.') return false; for (int i : {4, 5, 6, 7, 9, 10}) if (!isdigit((unsigned char)name[i])) return false; for (char c : name) if (!(isalnum((unsigned char)c) || c == '-' || c == '.' || c == ':')) return false; return true; } if (type == "eui." || type == "naa.") { std::string hex = name.substr(4); if (type == "eui." ? hex.size() != 16 : hex.size() != 16 && hex.size() != 32) return false; return hex.find_first_not_of("0123456789abcdef") == std::string::npos; } return false; } namespace { // "address:port" or "[v6]:port" bool validPortal(const std::string &p) { std::string addr, port; if (!p.empty() && p[0] == '[') { size_t close = p.find("]:"); if (close == std::string::npos) return false; addr = p.substr(1, close - 1); port = p.substr(close + 2); } else { size_t colon = p.find(':'); if (colon == std::string::npos || p.find(':', colon + 1) != std::string::npos) return false; addr = p.substr(0, colon); port = p.substr(colon + 1); } if (port.empty() || port.size() > 5 || port.find_first_not_of("0123456789") != std::string::npos) return false; int n = std::stoi(port); unsigned char buf[16]; bool v6 = p[0] == '['; return n >= 1 && n <= 65535 && inet_pton(v6 ? AF_INET6 : AF_INET, addr.c_str(), buf) == 1; } bool chapText(const std::string &v) { if (v.empty() || v.size() > 255) return false; for (unsigned char c : v) if (c < 0x20 || c == 0x7f) return false; return true; } } // namespace std::string checkIscsiTarget(IscsiTarget &t, const Config &cfg, const std::string &oldName, const std::string &lang) { t.name = lower(t.name); const std::string badName = tr(lang, "invalid target name: lower-case letters, digits, . - (max. 63)", "ungültiger Target-Name: Kleinbuchstaben, Ziffern, . - (max. 63)"); if (t.name.empty() || t.name.size() > 63 || !isalnum((unsigned char)t.name[0])) return badName; for (char c : t.name) if (!(isalnum((unsigned char)c) || c == '-' || c == '.')) return badName; for (const IscsiTarget &o : cfg.iscsiTargets) if (o.name == t.name && o.name != lower(oldName)) return tr(lang, "an iSCSI target %1 exists", "ein iSCSI-Target %1 gibt es schon", {t.name}); for (unsigned char c : t.comment) if (c < 0x20) return tr(lang, "invalid comment", "ungültiger Kommentar"); if (t.luns.empty()) return tr(lang, "the target needs at least one LUN", "das Target braucht mindestens eine LUN"); if (t.luns.size() > 64) return tr(lang, "at most 64 LUNs", "höchstens 64 LUNs"); for (size_t i = 0; i < t.luns.size(); ++i) { std::string &l = t.luns[i]; while (l.size() > 1 && l.back() == '/') l.pop_back(); if (l.empty() || l[0] != '/' || l.find("/../") != std::string::npos || l.find(',') != std::string::npos || l.find('\n') != std::string::npos) return tr(lang, "invalid LUN path: %1", "ungültiger LUN-Pfad: %1", {l}); struct stat st; if (stat(l.c_str(), &st) != 0) return tr(lang, "%1 doesn't exist", "%1 gibt es nicht", {l}); if (!S_ISBLK(st.st_mode) && !S_ISREG(st.st_mode)) return tr(lang, "%1 is neither a block device nor a file", "%1 ist weder ein Blockgerät noch eine Datei", {l}); if (S_ISREG(st.st_mode) && st.st_size < 1024 * 1024) return tr(lang, "the image file %1 is smaller than 1 MiB", "die Image-Datei %1 ist kleiner als 1 MiB", {l}); for (size_t j = 0; j < i; ++j) if (t.luns[j] == l) return tr(lang, "%1 is a LUN twice", "%1 ist zweimal LUN", {l}); for (const IscsiTarget &o : cfg.iscsiTargets) if (o.name != lower(oldName)) for (const std::string &ol : o.luns) if (ol == l) return tr(lang, "%1 is exported by %2 already", "%1 exportiert schon %2", {l, o.name}); } for (std::string &i : t.initiators) if (!validIscsiName(i)) return tr(lang, "invalid initiator name: %1", "ungültiger Initiator-Name: %1", {i}); if (t.open) t.initiators.clear(); else if (t.initiators.empty()) return tr(lang, "name the initiators that may connect (or allow any)", "die Initiatoren nennen, die sich verbinden dürfen (oder alle erlauben)"); if (t.chapUser.empty() && t.chapPassword.empty()) { t.chapUserIn.clear(); t.chapPasswordIn.clear(); return ""; } if (!chapText(t.chapUser) || !chapText(t.chapPassword)) return tr(lang, "CHAP needs a user name and a secret (up to 255 characters)", "CHAP braucht Benutzername und Passwort (bis 255 Zeichen)"); if (t.chapUserIn.empty() && t.chapPasswordIn.empty()) return ""; if (!chapText(t.chapUserIn) || !chapText(t.chapPasswordIn)) return tr(lang, "mutual CHAP needs the target's user name and secret", "gegenseitiges CHAP braucht Benutzername und Passwort des Targets"); if (t.chapPasswordIn == t.chapPassword) return tr(lang, "mutual CHAP: the two secrets must differ", "gegenseitiges CHAP: die beiden Passwörter müssen sich unterscheiden"); return ""; } std::string createShareDir(const Share &s, const std::string &lang) { struct stat st; if (stat(s.path.c_str(), &st) == 0) return ""; Loading Loading @@ -332,6 +466,16 @@ std::string checkSettings(const Settings &s, const std::string &lang) { if (s.nfsRoot.empty() || s.nfsRoot[0] != '/' || s.nfsRoot.find("/../") != std::string::npos || (s.nfsRoot.size() > 1 && s.nfsRoot.back() == '/')) return tr(lang, "the NFSv4 root must be an absolute directory", "die NFSv4-Wurzel muss ein absolutes Verzeichnis sein"); std::string base = s.iscsiBaseName; if (!validIscsiName(base) || base != s.iscsiBaseName) return tr(lang, "invalid iSCSI base name (iqn.YYYY-MM.domain:..., lower case)", "ungültiger iSCSI-Basisname (iqn.JJJJ-MM.domain:..., klein geschrieben)"); if (s.iscsiPortals.empty() || s.iscsiPortals.size() > 16) return tr(lang, "iSCSI needs 1 to 16 portals", "iSCSI braucht 1 bis 16 Portale"); for (const std::string &p : s.iscsiPortals) if (!validPortal(p)) return tr(lang, "invalid iSCSI portal %1 (address:port, [IPv6]:port)", "ungültiges iSCSI-Portal %1 (Adresse:Port, [IPv6]:Port)", {p}); return ""; } Loading Loading @@ -378,6 +522,27 @@ Config loadConfig(const std::string &path) { sh.fsid = (uint32_t)fsid; cfg.shares.push_back(sh); } readBool(conf, r + "/iscsi/enabled", s.iscsi); readString(conf, r + "/iscsi/base_name", s.iscsiBaseName); std::vector<std::string> portals = readList(conf, r + "/iscsi/portals"); if (!portals.empty()) s.iscsiPortals = portals; for (int i = 0;; ++i) { std::string p = r + "/iscsi/targets/" + std::to_string(i); if (!conf.getKey(p + "/name")) break; IscsiTarget t; readString(conf, p + "/name", t.name); readString(conf, p + "/comment", t.comment); t.luns = readList(conf, p + "/luns"); readBool(conf, p + "/read_only", t.readOnly); t.initiators = readList(conf, p + "/initiators"); readBool(conf, p + "/open", t.open); readString(conf, p + "/chap_user", t.chapUser); readString(conf, p + "/chap_password", t.chapPassword); readString(conf, p + "/chap_user_in", t.chapUserIn); readString(conf, p + "/chap_password_in", t.chapPasswordIn); cfg.iscsiTargets.push_back(t); } } catch (const confplus::ConfException &e) { throw std::runtime_error(path + ": " + e.what()); } Loading @@ -388,7 +553,7 @@ void saveConfig(const std::string &path, const Config &cfg) { // libconfplus loads before it can save: start from a minimal document std::string tmp = path + ".tmp"; { int fd = open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0644); int fd = open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0600); if (fd < 0) throw std::runtime_error(tmp + ": " + strerror(errno)); const char skel[] = "sharedd:\n version: 1\n"; bool ok = write(fd, skel, sizeof(skel) - 1) == (ssize_t)(sizeof(skel) - 1); Loading Loading @@ -427,11 +592,29 @@ void saveConfig(const std::string &path, const Config &cfg) { set(conf, p + "/async", yesNo(sh.async)); if (sh.fsid) set(conf, p + "/fsid", std::to_string(sh.fsid)); } set(conf, r + "/iscsi/enabled", yesNo(s.iscsi)); if (!s.iscsiBaseName.empty()) set(conf, r + "/iscsi/base_name", s.iscsiBaseName); setList(conf, r + "/iscsi/portals", s.iscsiPortals); for (size_t i = 0; i < cfg.iscsiTargets.size(); ++i) { const IscsiTarget &t = cfg.iscsiTargets[i]; std::string p = r + "/iscsi/targets/" + std::to_string(i); set(conf, p + "/name", t.name); if (!t.comment.empty()) set(conf, p + "/comment", t.comment); setList(conf, p + "/luns", t.luns); set(conf, p + "/read_only", yesNo(t.readOnly)); setList(conf, p + "/initiators", t.initiators); set(conf, p + "/open", yesNo(t.open)); if (!t.chapUser.empty()) set(conf, p + "/chap_user", t.chapUser); if (!t.chapPassword.empty()) set(conf, p + "/chap_password", t.chapPassword); if (!t.chapUserIn.empty()) set(conf, p + "/chap_user_in", t.chapUserIn); if (!t.chapPasswordIn.empty()) set(conf, p + "/chap_password_in", t.chapPasswordIn); } conf.saveConfig(tmp.c_str()); } catch (const confplus::ConfException &e) { unlink(tmp.c_str()); throw std::runtime_error(path + ": " + e.what()); } chmod(tmp.c_str(), 0600); // the CHAP secrets if (rename(tmp.c_str(), path.c_str()) != 0) { int err = errno; unlink(tmp.c_str()); Loading src/config.h +42 −0 Changes for src/config.h: 42 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -42,6 +42,18 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * users: [jan, "@staff"] * nfs_clients: ["192.168.1.0/24"] * ... * iscsi: * enabled: true * base_name: iqn.2026-01.de.tuxist.fenriros:nas * portals: ["0.0.0.0:3260"] * targets: * - name: vm1 * luns: [/dev/vg0/vm1] * initiators: [iqn.2005-03.org.open-iscsi:abc] * chap_user: vm1 * chap_password: ... * * The file is mode 0600: it keeps the iSCSI CHAP secrets. * * See include/sharedd/api.h for what each field means. */ Loading Loading @@ -69,6 +81,22 @@ struct Share { uint32_t fsid = 0; // 0: derived from the path }; // An iSCSI target exported by the kernel's LIO (iscsi.h). Its name is // Settings::iscsiBaseName + ":" + name. struct IscsiTarget { std::string name; // [a-z0-9.-], max. 63 std::string comment; std::vector<std::string> luns; // block devices (LVM volumes, disks, md) or image files; LUN 0, 1, ... bool readOnly = false; std::vector<std::string> initiators; // the initiators' IQNs allowed in bool open = false; // any initiator (no list) -- for a lab std::string chapUser, chapPassword; // empty: no CHAP std::string chapUserIn, chapPasswordIn; // mutual CHAP: the target's credentials bool operator==(const IscsiTarget &o) const; bool operator!=(const IscsiTarget &o) const { return !(*this == o); } }; struct Settings { bool smb = true; std::string workgroup = "WORKGROUP"; Loading @@ -86,14 +114,20 @@ struct Settings { // be on an exportable filesystem -- not overlayfs, so on the live // system a directory on a disk or tmpfs. std::string nfsRoot = "/"; bool iscsi = true; std::string iscsiBaseName; // empty: made up once (defaultIscsiBaseName()) and kept std::vector<std::string> iscsiPortals = {"0.0.0.0:3260"}; // address:port, [v6]:port }; struct Config { Settings settings; std::vector<Share> shares; std::vector<IscsiTarget> iscsiTargets; const Share *find(const std::string &name) const; // SMB names are case-insensitive Share *find(const std::string &name); IscsiTarget *findIscsi(const std::string &name); std::string iqn(const IscsiTarget &t) const { return settings.iscsiBaseName + ":" + t.name; } }; // Missing file: the defaults. Throws std::runtime_error if it can't be read. Loading Loading @@ -125,6 +159,14 @@ uint32_t pathFsid(const std::string &path); bool sameName(const std::string &a, const std::string &b); // iqn.2026-01.de.tuxist.fenriros:<host name> std::string defaultIscsiBaseName(); // an iSCSI name (iqn./eui./naa.); lower-cases it bool validIscsiName(std::string &name); // "" if t can be stored in cfg (replacing the target named oldName, if // given), else why not (in lang). Normalizes it. std::string checkIscsiTarget(IscsiTarget &t, const Config &cfg, const std::string &oldName = "", const std::string &lang = "en"); // The NetBIOS name: the setting, else the host name; upper case, max. 15. std::string netbiosName(const Settings &s); Loading Loading
CMakeLists.txt +5 −2 Changes for CMakeLists.txt: 5 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -2,7 +2,8 @@ cmake_minimum_required(VERSION 3.16) project(sharedd VERSION 0.1.0 LANGUAGES CXX) # sharedd: SMB and NFS shares served by the kernel's own file servers -- # ksmbd for SMB2/3 and knfsd for NFSv4. sharedd is what the kernel asks # ksmbd for SMB2/3 and knfsd for NFSv4 -- and iSCSI targets (LVM volumes, # disks, image files) served by the kernel's LIO, set up over configfs. sharedd is what the kernel asks # who may log in and which share means which directory (in place of # ksmbd.mountd and rpc.mountd), keeps the shares in /etc/sharedd/sharedd.yaml # (libconfplus) and offers them on sysbus as the service "sharedd". Loading Loading @@ -34,6 +35,7 @@ add_executable(sharedd src/nfs.cpp src/rpc.cpp src/files.cpp src/iscsi.cpp ) target_include_directories(sharedd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src) target_compile_definitions(sharedd PRIVATE SHAREDD_CONFIG="${SHAREDD_CONFIG}") Loading @@ -45,6 +47,7 @@ target_link_libraries(sharectl PRIVATE sysbus::sysbus) install(TARGETS sharedd sharectl RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR}) install(DIRECTORY include/sharedd DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(FILES dist/sharedd.yaml DESTINATION /etc/sharedd) # iSCSI CHAP secrets: root only install(FILES dist/sharedd.yaml DESTINATION /etc/sharedd PERMISSIONS OWNER_READ OWNER_WRITE) set(SHAREDD_UNITDIR "/etc/syslite/services" CACHE PATH "syslite unit directory") install(FILES dist/sharedd.service DESTINATION ${SHAREDD_UNITDIR})
dist/sharedd.yaml +11 −0 Changes for dist/sharedd.yaml: 11 added lines, 0 removed lines. Original line number Diff line number Diff line # sharedd: SMB and NFS shares (served by the kernel's ksmbd and knfsd). # Edit with sharectl or webctl; by hand, then: sharectl reload # Keeps iSCSI CHAP secrets: mode 0600. sharedd: smb: enabled: true Loading @@ -23,3 +24,13 @@ sharedd: # nfs: true # nfs_clients: ["192.168.1.0/24"] # squash: root # root|all|none iscsi: enabled: true # LIO, the kernel's iSCSI target (port 3260) portals: ["0.0.0.0:3260"] # base_name: iqn.2026-01.de.tuxist.fenriros:<host> # made up on the first start # targets: # - name: vm1 # the target is <base_name>:vm1 # luns: [/dev/vg0/vm1] # LVM volumes, disks, md arrays or image files # initiators: [iqn.2005-03.org.open-iscsi:1234abcd] # chap_user: vm1 # optional; mutual: chap_user_in, chap_password_in # chap_password: secret12345678
include/sharedd/api.h +34 −3 Changes for include/sharedd/api.h: 34 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -52,10 +52,32 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * min_protocol, max_protocol (SMB2_10 SMB3_00 SMB3_02 SMB3_11), signing * (disabled|enabled|auto|mandatory), map_to_guest (never|bad_user), * guest_account, nfs (yes|no), nfs_threads, nfs_root (the directory NFSv4 * clients see as "/"; NFS shares have to be below it). * clients see as "/"; NFS shares have to be below it), iscsi (yes|no), * iscsi_base_name (targets are <base>:<name>), iscsi_portal (repeated: * address:port or [IPv6]:port to listen on). * * Status: smb_state / nfs_state (disabled|running|failed), smb_error / * nfs_error, shares. */ * Status: smb_state / nfs_state / iscsi_state (disabled|running|failed), * smb_error / nfs_error / iscsi_error, shares, iscsi_targets. * * An iSCSI target (served by the kernel's LIO), in replies and as * arguments: * name [a-z0-9.-] (max. 63); the target is <base name>:<name> * iqn replies only * comment free text * lun repeated, LUN 0, 1, ...: a block device (LVM volume, * md array, disk, partition) or an image file (>= 1 MiB) * read_only yes|no * open yes|no: any initiator may connect (no ACL list) * initiator repeated: the initiators' IQNs allowed in * chap_user, chap_password CHAP (empty: none); replies have * chap_password_set instead * chap_user_in, chap_password_in mutual CHAP: what the target answers * with; replies: chap_password_in_set * ListIscsiTargets repeats these per target, each starting with its * "name", plus "session" (repeated: initiators logged in) and "error" (why * it couldn't be set up, e.g. the volume is mounted here). EditIscsiTarget * takes name + the whole new target (rename: new_name); a missing * chap_password/chap_password_in keeps the stored one. */ #pragma once Loading @@ -74,6 +96,15 @@ constexpr const char *GetSettings = "GetSettings"; constexpr const char *SetSettings = "SetSettings"; // the settings to change constexpr const char *Reload = "Reload"; // re-read the configuration file // iSCSI targets (changes: root only) constexpr const char *ListIscsiTargets = "ListIscsiTargets"; constexpr const char *CreateIscsiTarget = "CreateIscsiTarget"; // a target constexpr const char *EditIscsiTarget = "EditIscsiTarget"; // name + the whole new target constexpr const char *DeleteIscsiTarget = "DeleteIscsiTarget"; // name // block devices that could be LUNs -> per device: device (path), kind // (lvm|md|disk|partition), size (bytes), in_use (why not, "" if free) constexpr const char *ListIscsiDevices = "ListIscsiDevices"; // Files and folders inside a share (root only). path: inside the share, // "" for its own folder; ".." and symlinks can't lead out of it. // Browse {share, path} -> path, self_owner/self_group/self_mode (the Loading
src/config.cpp +184 −1 Changes for src/config.cpp: 184 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -191,6 +191,140 @@ Share *Config::find(const std::string &name) { return nullptr; } IscsiTarget *Config::findIscsi(const std::string &name) { for (IscsiTarget &t : iscsiTargets) if (t.name == lower(name)) return &t; return nullptr; } bool IscsiTarget::operator==(const IscsiTarget &o) const { return name == o.name && comment == o.comment && luns == o.luns && readOnly == o.readOnly && initiators == o.initiators && open == o.open && chapUser == o.chapUser && chapPassword == o.chapPassword && chapUserIn == o.chapUserIn && chapPasswordIn == o.chapPasswordIn; } std::string defaultIscsiBaseName() { char host[256] = ""; gethostname(host, sizeof(host) - 1); std::string h; for (char c : std::string(host).substr(0, std::string(host).find('.'))) if (isalnum((unsigned char)c) || c == '-') h += (char)tolower((unsigned char)c); return "iqn.2026-01.de.tuxist.fenriros:" + (h.empty() ? std::string("fenriros") : h); } bool validIscsiName(std::string &name) { name = lower(name); if (name.size() < 5 || name.size() > 223) return false; std::string type = name.substr(0, 4); if (type == "iqn.") { // iqn.YYYY-MM.reversed.domain[:anything] if (name.size() < 12 || name[8] != '-' || name[11] != '.') return false; for (int i : {4, 5, 6, 7, 9, 10}) if (!isdigit((unsigned char)name[i])) return false; for (char c : name) if (!(isalnum((unsigned char)c) || c == '-' || c == '.' || c == ':')) return false; return true; } if (type == "eui." || type == "naa.") { std::string hex = name.substr(4); if (type == "eui." ? hex.size() != 16 : hex.size() != 16 && hex.size() != 32) return false; return hex.find_first_not_of("0123456789abcdef") == std::string::npos; } return false; } namespace { // "address:port" or "[v6]:port" bool validPortal(const std::string &p) { std::string addr, port; if (!p.empty() && p[0] == '[') { size_t close = p.find("]:"); if (close == std::string::npos) return false; addr = p.substr(1, close - 1); port = p.substr(close + 2); } else { size_t colon = p.find(':'); if (colon == std::string::npos || p.find(':', colon + 1) != std::string::npos) return false; addr = p.substr(0, colon); port = p.substr(colon + 1); } if (port.empty() || port.size() > 5 || port.find_first_not_of("0123456789") != std::string::npos) return false; int n = std::stoi(port); unsigned char buf[16]; bool v6 = p[0] == '['; return n >= 1 && n <= 65535 && inet_pton(v6 ? AF_INET6 : AF_INET, addr.c_str(), buf) == 1; } bool chapText(const std::string &v) { if (v.empty() || v.size() > 255) return false; for (unsigned char c : v) if (c < 0x20 || c == 0x7f) return false; return true; } } // namespace std::string checkIscsiTarget(IscsiTarget &t, const Config &cfg, const std::string &oldName, const std::string &lang) { t.name = lower(t.name); const std::string badName = tr(lang, "invalid target name: lower-case letters, digits, . - (max. 63)", "ungültiger Target-Name: Kleinbuchstaben, Ziffern, . - (max. 63)"); if (t.name.empty() || t.name.size() > 63 || !isalnum((unsigned char)t.name[0])) return badName; for (char c : t.name) if (!(isalnum((unsigned char)c) || c == '-' || c == '.')) return badName; for (const IscsiTarget &o : cfg.iscsiTargets) if (o.name == t.name && o.name != lower(oldName)) return tr(lang, "an iSCSI target %1 exists", "ein iSCSI-Target %1 gibt es schon", {t.name}); for (unsigned char c : t.comment) if (c < 0x20) return tr(lang, "invalid comment", "ungültiger Kommentar"); if (t.luns.empty()) return tr(lang, "the target needs at least one LUN", "das Target braucht mindestens eine LUN"); if (t.luns.size() > 64) return tr(lang, "at most 64 LUNs", "höchstens 64 LUNs"); for (size_t i = 0; i < t.luns.size(); ++i) { std::string &l = t.luns[i]; while (l.size() > 1 && l.back() == '/') l.pop_back(); if (l.empty() || l[0] != '/' || l.find("/../") != std::string::npos || l.find(',') != std::string::npos || l.find('\n') != std::string::npos) return tr(lang, "invalid LUN path: %1", "ungültiger LUN-Pfad: %1", {l}); struct stat st; if (stat(l.c_str(), &st) != 0) return tr(lang, "%1 doesn't exist", "%1 gibt es nicht", {l}); if (!S_ISBLK(st.st_mode) && !S_ISREG(st.st_mode)) return tr(lang, "%1 is neither a block device nor a file", "%1 ist weder ein Blockgerät noch eine Datei", {l}); if (S_ISREG(st.st_mode) && st.st_size < 1024 * 1024) return tr(lang, "the image file %1 is smaller than 1 MiB", "die Image-Datei %1 ist kleiner als 1 MiB", {l}); for (size_t j = 0; j < i; ++j) if (t.luns[j] == l) return tr(lang, "%1 is a LUN twice", "%1 ist zweimal LUN", {l}); for (const IscsiTarget &o : cfg.iscsiTargets) if (o.name != lower(oldName)) for (const std::string &ol : o.luns) if (ol == l) return tr(lang, "%1 is exported by %2 already", "%1 exportiert schon %2", {l, o.name}); } for (std::string &i : t.initiators) if (!validIscsiName(i)) return tr(lang, "invalid initiator name: %1", "ungültiger Initiator-Name: %1", {i}); if (t.open) t.initiators.clear(); else if (t.initiators.empty()) return tr(lang, "name the initiators that may connect (or allow any)", "die Initiatoren nennen, die sich verbinden dürfen (oder alle erlauben)"); if (t.chapUser.empty() && t.chapPassword.empty()) { t.chapUserIn.clear(); t.chapPasswordIn.clear(); return ""; } if (!chapText(t.chapUser) || !chapText(t.chapPassword)) return tr(lang, "CHAP needs a user name and a secret (up to 255 characters)", "CHAP braucht Benutzername und Passwort (bis 255 Zeichen)"); if (t.chapUserIn.empty() && t.chapPasswordIn.empty()) return ""; if (!chapText(t.chapUserIn) || !chapText(t.chapPasswordIn)) return tr(lang, "mutual CHAP needs the target's user name and secret", "gegenseitiges CHAP braucht Benutzername und Passwort des Targets"); if (t.chapPasswordIn == t.chapPassword) return tr(lang, "mutual CHAP: the two secrets must differ", "gegenseitiges CHAP: die beiden Passwörter müssen sich unterscheiden"); return ""; } std::string createShareDir(const Share &s, const std::string &lang) { struct stat st; if (stat(s.path.c_str(), &st) == 0) return ""; Loading Loading @@ -332,6 +466,16 @@ std::string checkSettings(const Settings &s, const std::string &lang) { if (s.nfsRoot.empty() || s.nfsRoot[0] != '/' || s.nfsRoot.find("/../") != std::string::npos || (s.nfsRoot.size() > 1 && s.nfsRoot.back() == '/')) return tr(lang, "the NFSv4 root must be an absolute directory", "die NFSv4-Wurzel muss ein absolutes Verzeichnis sein"); std::string base = s.iscsiBaseName; if (!validIscsiName(base) || base != s.iscsiBaseName) return tr(lang, "invalid iSCSI base name (iqn.YYYY-MM.domain:..., lower case)", "ungültiger iSCSI-Basisname (iqn.JJJJ-MM.domain:..., klein geschrieben)"); if (s.iscsiPortals.empty() || s.iscsiPortals.size() > 16) return tr(lang, "iSCSI needs 1 to 16 portals", "iSCSI braucht 1 bis 16 Portale"); for (const std::string &p : s.iscsiPortals) if (!validPortal(p)) return tr(lang, "invalid iSCSI portal %1 (address:port, [IPv6]:port)", "ungültiges iSCSI-Portal %1 (Adresse:Port, [IPv6]:Port)", {p}); return ""; } Loading Loading @@ -378,6 +522,27 @@ Config loadConfig(const std::string &path) { sh.fsid = (uint32_t)fsid; cfg.shares.push_back(sh); } readBool(conf, r + "/iscsi/enabled", s.iscsi); readString(conf, r + "/iscsi/base_name", s.iscsiBaseName); std::vector<std::string> portals = readList(conf, r + "/iscsi/portals"); if (!portals.empty()) s.iscsiPortals = portals; for (int i = 0;; ++i) { std::string p = r + "/iscsi/targets/" + std::to_string(i); if (!conf.getKey(p + "/name")) break; IscsiTarget t; readString(conf, p + "/name", t.name); readString(conf, p + "/comment", t.comment); t.luns = readList(conf, p + "/luns"); readBool(conf, p + "/read_only", t.readOnly); t.initiators = readList(conf, p + "/initiators"); readBool(conf, p + "/open", t.open); readString(conf, p + "/chap_user", t.chapUser); readString(conf, p + "/chap_password", t.chapPassword); readString(conf, p + "/chap_user_in", t.chapUserIn); readString(conf, p + "/chap_password_in", t.chapPasswordIn); cfg.iscsiTargets.push_back(t); } } catch (const confplus::ConfException &e) { throw std::runtime_error(path + ": " + e.what()); } Loading @@ -388,7 +553,7 @@ void saveConfig(const std::string &path, const Config &cfg) { // libconfplus loads before it can save: start from a minimal document std::string tmp = path + ".tmp"; { int fd = open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0644); int fd = open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0600); if (fd < 0) throw std::runtime_error(tmp + ": " + strerror(errno)); const char skel[] = "sharedd:\n version: 1\n"; bool ok = write(fd, skel, sizeof(skel) - 1) == (ssize_t)(sizeof(skel) - 1); Loading Loading @@ -427,11 +592,29 @@ void saveConfig(const std::string &path, const Config &cfg) { set(conf, p + "/async", yesNo(sh.async)); if (sh.fsid) set(conf, p + "/fsid", std::to_string(sh.fsid)); } set(conf, r + "/iscsi/enabled", yesNo(s.iscsi)); if (!s.iscsiBaseName.empty()) set(conf, r + "/iscsi/base_name", s.iscsiBaseName); setList(conf, r + "/iscsi/portals", s.iscsiPortals); for (size_t i = 0; i < cfg.iscsiTargets.size(); ++i) { const IscsiTarget &t = cfg.iscsiTargets[i]; std::string p = r + "/iscsi/targets/" + std::to_string(i); set(conf, p + "/name", t.name); if (!t.comment.empty()) set(conf, p + "/comment", t.comment); setList(conf, p + "/luns", t.luns); set(conf, p + "/read_only", yesNo(t.readOnly)); setList(conf, p + "/initiators", t.initiators); set(conf, p + "/open", yesNo(t.open)); if (!t.chapUser.empty()) set(conf, p + "/chap_user", t.chapUser); if (!t.chapPassword.empty()) set(conf, p + "/chap_password", t.chapPassword); if (!t.chapUserIn.empty()) set(conf, p + "/chap_user_in", t.chapUserIn); if (!t.chapPasswordIn.empty()) set(conf, p + "/chap_password_in", t.chapPasswordIn); } conf.saveConfig(tmp.c_str()); } catch (const confplus::ConfException &e) { unlink(tmp.c_str()); throw std::runtime_error(path + ": " + e.what()); } chmod(tmp.c_str(), 0600); // the CHAP secrets if (rename(tmp.c_str(), path.c_str()) != 0) { int err = errno; unlink(tmp.c_str()); Loading
src/config.h +42 −0 Changes for src/config.h: 42 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -42,6 +42,18 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * users: [jan, "@staff"] * nfs_clients: ["192.168.1.0/24"] * ... * iscsi: * enabled: true * base_name: iqn.2026-01.de.tuxist.fenriros:nas * portals: ["0.0.0.0:3260"] * targets: * - name: vm1 * luns: [/dev/vg0/vm1] * initiators: [iqn.2005-03.org.open-iscsi:abc] * chap_user: vm1 * chap_password: ... * * The file is mode 0600: it keeps the iSCSI CHAP secrets. * * See include/sharedd/api.h for what each field means. */ Loading Loading @@ -69,6 +81,22 @@ struct Share { uint32_t fsid = 0; // 0: derived from the path }; // An iSCSI target exported by the kernel's LIO (iscsi.h). Its name is // Settings::iscsiBaseName + ":" + name. struct IscsiTarget { std::string name; // [a-z0-9.-], max. 63 std::string comment; std::vector<std::string> luns; // block devices (LVM volumes, disks, md) or image files; LUN 0, 1, ... bool readOnly = false; std::vector<std::string> initiators; // the initiators' IQNs allowed in bool open = false; // any initiator (no list) -- for a lab std::string chapUser, chapPassword; // empty: no CHAP std::string chapUserIn, chapPasswordIn; // mutual CHAP: the target's credentials bool operator==(const IscsiTarget &o) const; bool operator!=(const IscsiTarget &o) const { return !(*this == o); } }; struct Settings { bool smb = true; std::string workgroup = "WORKGROUP"; Loading @@ -86,14 +114,20 @@ struct Settings { // be on an exportable filesystem -- not overlayfs, so on the live // system a directory on a disk or tmpfs. std::string nfsRoot = "/"; bool iscsi = true; std::string iscsiBaseName; // empty: made up once (defaultIscsiBaseName()) and kept std::vector<std::string> iscsiPortals = {"0.0.0.0:3260"}; // address:port, [v6]:port }; struct Config { Settings settings; std::vector<Share> shares; std::vector<IscsiTarget> iscsiTargets; const Share *find(const std::string &name) const; // SMB names are case-insensitive Share *find(const std::string &name); IscsiTarget *findIscsi(const std::string &name); std::string iqn(const IscsiTarget &t) const { return settings.iscsiBaseName + ":" + t.name; } }; // Missing file: the defaults. Throws std::runtime_error if it can't be read. Loading Loading @@ -125,6 +159,14 @@ uint32_t pathFsid(const std::string &path); bool sameName(const std::string &a, const std::string &b); // iqn.2026-01.de.tuxist.fenriros:<host name> std::string defaultIscsiBaseName(); // an iSCSI name (iqn./eui./naa.); lower-cases it bool validIscsiName(std::string &name); // "" if t can be stored in cfg (replacing the target named oldName, if // given), else why not (in lang). Normalizes it. std::string checkIscsiTarget(IscsiTarget &t, const Config &cfg, const std::string &oldName = "", const std::string &lang = "en"); // The NetBIOS name: the setting, else the host name; upper case, max. 15. std::string netbiosName(const Settings &s); Loading