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 664fa1b03690..9cf27e56827a 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 c65428ea7e7d..58a1b23d17bf 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 6430993d5c63..ff95d6500b9c 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 000000000000..fea9bc1ff71a --- /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 000000000000..87f8c41a8e5c --- /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 15b54cd9fe1d..043d8a56d0c6 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 000000000000..16255e68e8a1 --- /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 95c2f970999a..131793894841 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.