diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
index 664fa1b0369..9cf27e56827 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -256,6 +256,13 @@
services.ethercalc.
+
+
+ nbd, a
+ Network Block Device server. Available as
+ services.nbd.
+
+
timetagger,
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index c65428ea7e7..58a1b23d17b 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -75,6 +75,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [ethercalc](https://github.com/audreyt/ethercalc), an online collaborative
spreadsheet. Available as [services.ethercalc](options.html#opt-services.ethercalc.enable).
+- [nbd](https://nbd.sourceforge.io/), a Network Block Device server. Available as [services.nbd](options.html#opt-services.nbd.server.enable).
+
- [timetagger](https://timetagger.app), an open source time-tracker with an intuitive user experience and powerful reporting. [services.timetagger](options.html#opt-services.timetagger.enable).
- [rstudio-server](https://www.rstudio.com/products/rstudio/#rstudio-server), a browser-based version of the RStudio IDE for the R programming language. Available as [services.rstudio-server](options.html#opt-services.rstudio-server.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 6430993d5c6..ff95d6500b9 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -180,6 +180,7 @@
./programs/msmtp.nix
./programs/mtr.nix
./programs/nano.nix
+ ./programs/nbd.nix
./programs/neovim.nix
./programs/nm-applet.nix
./programs/npm.nix
@@ -819,6 +820,7 @@
./services/networking/nar-serve.nix
./services/networking/nat.nix
./services/networking/nats.nix
+ ./services/networking/nbd.nix
./services/networking/ndppd.nix
./services/networking/nebula.nix
./services/networking/networkmanager.nix
diff --git a/nixos/modules/programs/nbd.nix b/nixos/modules/programs/nbd.nix
new file mode 100644
index 00000000000..fea9bc1ff71
--- /dev/null
+++ b/nixos/modules/programs/nbd.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.programs.nbd;
+in
+{
+ options = {
+ programs.nbd = {
+ enable = mkEnableOption "Network Block Device (nbd) support";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ nbd ];
+ boot.kernelModules = [ "nbd" ];
+ };
+}
diff --git a/nixos/modules/services/networking/nbd.nix b/nixos/modules/services/networking/nbd.nix
new file mode 100644
index 00000000000..87f8c41a8e5
--- /dev/null
+++ b/nixos/modules/services/networking/nbd.nix
@@ -0,0 +1,146 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.nbd;
+ configFormat = pkgs.formats.ini { };
+ iniFields = with types; attrsOf (oneOf [ bool int float str ]);
+ serverConfig = configFormat.generate "nbd-server-config"
+ ({
+ generic =
+ (cfg.server.extraOptions // {
+ user = "root";
+ group = "root";
+ port = cfg.server.listenPort;
+ } // (optionalAttrs (cfg.server.listenAddress != null) {
+ listenaddr = cfg.server.listenAddress;
+ }));
+ }
+ // (mapAttrs
+ (_: { path, allowAddresses, extraOptions }:
+ extraOptions // {
+ exportname = path;
+ } // (optionalAttrs (allowAddresses != null) {
+ authfile = pkgs.writeText "authfile" (concatStringsSep "\n" allowAddresses);
+ }))
+ cfg.server.exports)
+ );
+ splitLists =
+ partition
+ (path: hasPrefix "/dev/" path)
+ (mapAttrsToList (_: { path, ... }: path) cfg.server.exports);
+ allowedDevices = splitLists.right;
+ boundPaths = splitLists.wrong;
+in
+{
+ options = {
+ services.nbd = {
+ server = {
+ enable = mkEnableOption "the Network Block Device (nbd) server";
+
+ listenPort = mkOption {
+ type = types.port;
+ default = 10809;
+ description = "Port to listen on. The port is NOT automatically opened in the firewall.";
+ };
+
+ extraOptions = mkOption {
+ type = iniFields;
+ default = {
+ allowlist = false;
+ };
+ description = ''
+ Extra options for the server. See
+ nbd-server
+ 5.
+ '';
+ };
+
+ exports = mkOption {
+ description = "Files or block devices to make available over the network.";
+ default = { };
+ type = with types; attrsOf
+ (submodule {
+ options = {
+ path = mkOption {
+ type = str;
+ description = "File or block device to export.";
+ example = "/dev/sdb1";
+ };
+
+ allowAddresses = mkOption {
+ type = nullOr (listOf str);
+ default = null;
+ example = [ "10.10.0.0/24" "127.0.0.1" ];
+ description = "IPs and subnets that are authorized to connect for this device. If not specified, the server will allow all connections.";
+ };
+
+ extraOptions = mkOption {
+ type = iniFields;
+ default = {
+ flush = true;
+ fua = true;
+ };
+ description = ''
+ Extra options for this export. See
+ nbd-server
+ 5.
+ '';
+ };
+ };
+ });
+ };
+
+ listenAddress = mkOption {
+ type = with types; nullOr str;
+ description = "Address to listen on. If not specified, the server will listen on all interfaces.";
+ default = null;
+ example = "10.10.0.1";
+ };
+ };
+ };
+ };
+
+ config = mkIf cfg.server.enable {
+ boot.kernelModules = [ "nbd" ];
+
+ systemd.services.nbd-server = {
+ after = [ "network-online.target" ];
+ before = [ "multi-user.target" ];
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig = {
+ ExecStart = "${pkgs.nbd}/bin/nbd-server -C ${serverConfig}";
+ Type = "forking";
+
+ DeviceAllow = map (path: "${path} rw") allowedDevices;
+ BindPaths = boundPaths;
+
+ CapabilityBoundingSet = "";
+ DevicePolicy = "closed";
+ LockPersonality = true;
+ MemoryDenyWriteExecute = true;
+ NoNewPrivileges = true;
+ PrivateDevices = false;
+ PrivateMounts = true;
+ PrivateTmp = true;
+ PrivateUsers = true;
+ ProcSubset = "pid";
+ ProtectClock = true;
+ ProtectControlGroups = true;
+ ProtectHome = true;
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ ProtectProc = "noaccess";
+ ProtectSystem = "strict";
+ RestrictAddressFamilies = "AF_INET AF_INET6";
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ UMask = "0077";
+ };
+ };
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 15b54cd9fe1..043d8a56d0c 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -329,6 +329,7 @@ in
nat.standalone = handleTest ./nat.nix { withFirewall = false; };
nats = handleTest ./nats.nix {};
navidrome = handleTest ./navidrome.nix {};
+ nbd = handleTest ./nbd.nix {};
ncdns = handleTest ./ncdns.nix {};
ndppd = handleTest ./ndppd.nix {};
nebula = handleTest ./nebula.nix {};
diff --git a/nixos/tests/nbd.nix b/nixos/tests/nbd.nix
new file mode 100644
index 00000000000..16255e68e8a
--- /dev/null
+++ b/nixos/tests/nbd.nix
@@ -0,0 +1,87 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+ let
+ listenPort = 30123;
+ testString = "It works!";
+ mkCreateSmallFileService = { path, loop ? false }: {
+ script = ''
+ ${pkgs.coreutils}/bin/dd if=/dev/zero of=${path} bs=1K count=100
+ ${pkgs.lib.optionalString loop
+ "${pkgs.util-linux}/bin/losetup --find ${path}"}
+ '';
+ serviceConfig = {
+ Type = "oneshot";
+ };
+ wantedBy = [ "multi-user.target" ];
+ before = [ "nbd-server.service" ];
+ };
+ in
+ {
+ name = "nbd";
+
+ nodes = {
+ server = { config, pkgs, ... }: {
+ # Create some small files of zeros to use as the ndb disks
+ ## `vault-pub.disk` is accessible from any IP
+ systemd.services.create-pub-file =
+ mkCreateSmallFileService { path = "/vault-pub.disk"; };
+ ## `vault-priv.disk` is accessible only from localhost.
+ ## It's also a loopback device to test exporting /dev/...
+ systemd.services.create-priv-file =
+ mkCreateSmallFileService { path = "/vault-priv.disk"; loop = true; };
+
+ # Needed only for nbd-client used in the tests.
+ environment.systemPackages = [ pkgs.nbd ];
+
+ # Open the nbd port in the firewall
+ networking.firewall.allowedTCPPorts = [ listenPort ];
+
+ # Run the nbd server and expose the small file created above
+ services.nbd.server = {
+ enable = true;
+ exports = {
+ vault-pub = {
+ path = "/vault-pub.disk";
+ };
+ vault-priv = {
+ path = "/dev/loop0";
+ allowAddresses = [ "127.0.0.1" "::1" ];
+ };
+ };
+ listenAddress = "0.0.0.0";
+ listenPort = listenPort;
+ };
+ };
+
+ client = { config, pkgs, ... }: {
+ programs.nbd.enable = true;
+ };
+ };
+
+ testScript = ''
+ testString = "${testString}"
+
+ start_all()
+ server.wait_for_open_port(${toString listenPort})
+
+ # Client: Connect to the server, write a small string to the nbd disk, and cleanly disconnect
+ client.succeed("nbd-client server ${toString listenPort} /dev/nbd0 -name vault-pub -persist")
+ client.succeed(f"echo '{testString}' | dd of=/dev/nbd0 conv=notrunc")
+ client.succeed("nbd-client -d /dev/nbd0")
+
+ # Server: Check that the string written by the client is indeed in the file
+ foundString = server.succeed(f"dd status=none if=/vault-pub.disk count={len(testString)}")[:len(testString)]
+ if foundString != testString:
+ raise Exception(f"Read the wrong string from nbd disk. Expected: '{testString}'. Found: '{foundString}'")
+
+ # Client: Fail to connect to the private disk
+ client.fail("nbd-client server ${toString listenPort} /dev/nbd0 -name vault-priv -persist")
+
+ # Server: Successfully connect to the private disk
+ server.succeed("nbd-client localhost ${toString listenPort} /dev/nbd0 -name vault-priv -persist")
+ server.succeed(f"echo '{testString}' | dd of=/dev/nbd0 conv=notrunc")
+ foundString = server.succeed(f"dd status=none if=/dev/loop0 count={len(testString)}")[:len(testString)]
+ if foundString != testString:
+ raise Exception(f"Read the wrong string from nbd disk. Expected: '{testString}'. Found: '{foundString}'")
+ server.succeed("nbd-client -d /dev/nbd0")
+ '';
+ })
diff --git a/pkgs/tools/networking/nbd/default.nix b/pkgs/tools/networking/nbd/default.nix
index 95c2f970999..13179389484 100644
--- a/pkgs/tools/networking/nbd/default.nix
+++ b/pkgs/tools/networking/nbd/default.nix
@@ -1,4 +1,4 @@
-{ lib, stdenv, fetchurl, pkg-config, glib, which }:
+{ lib, stdenv, fetchurl, pkg-config, glib, which, nixosTests }:
stdenv.mkDerivation rec {
pname = "nbd";
@@ -21,6 +21,10 @@ stdenv.mkDerivation rec {
doCheck = true;
+ passthru.tests = {
+ test = nixosTests.nbd;
+ };
+
# Glib calls `clock_gettime', which is in librt. Linking that library
# here ensures that a proper rpath is added to the executable so that
# it can be loaded at run-time.