1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.nbd;
7 iniFields = with types; attrsOf (oneOf [ bool int float str ]);
8 # The `[generic]` section must come before all the others in the
9 # config file. This means we can't just dump an attrset to INI
10 # because that sorts the sections by name. Instead, we serialize it
11 # on its own first.
12 genericSection = {
13 generic = (cfg.server.extraOptions // {
14 user = "root";
15 group = "root";
16 port = cfg.server.listenPort;
17 } // (optionalAttrs (cfg.server.listenAddress != null) {
18 listenaddr = cfg.server.listenAddress;
19 }));
20 };
21 exportSections =
22 mapAttrs
23 (_: { path, allowAddresses, extraOptions }:
24 extraOptions // {
25 exportname = path;
26 } // (optionalAttrs (allowAddresses != null) {
27 authfile = pkgs.writeText "authfile" (concatStringsSep "\n" allowAddresses);
28 }))
29 cfg.server.exports;
30 serverConfig =
31 pkgs.writeText "nbd-server-config" ''
32 ${lib.generators.toINI {} genericSection}
33 ${lib.generators.toINI {} exportSections}
34 '';
35 splitLists =
36 partition
37 (path: hasPrefix "/dev/" path)
38 (mapAttrsToList (_: { path, ... }: path) cfg.server.exports);
39 allowedDevices = splitLists.right;
40 boundPaths = splitLists.wrong;
41in
42{
43 options = {
44 services.nbd = {
45 server = {
46 enable = mkEnableOption (lib.mdDoc "the Network Block Device (nbd) server");
47
48 listenPort = mkOption {
49 type = types.port;
50 default = 10809;
51 description = lib.mdDoc "Port to listen on. The port is NOT automatically opened in the firewall.";
52 };
53
54 extraOptions = mkOption {
55 type = iniFields;
56 default = {
57 allowlist = false;
58 };
59 description = lib.mdDoc ''
60 Extra options for the server. See
61 {manpage}`nbd-server(5)`.
62 '';
63 };
64
65 exports = mkOption {
66 description = lib.mdDoc "Files or block devices to make available over the network.";
67 default = { };
68 type = with types; attrsOf
69 (submodule {
70 options = {
71 path = mkOption {
72 type = str;
73 description = lib.mdDoc "File or block device to export.";
74 example = "/dev/sdb1";
75 };
76
77 allowAddresses = mkOption {
78 type = nullOr (listOf str);
79 default = null;
80 example = [ "10.10.0.0/24" "127.0.0.1" ];
81 description = lib.mdDoc "IPs and subnets that are authorized to connect for this device. If not specified, the server will allow all connections.";
82 };
83
84 extraOptions = mkOption {
85 type = iniFields;
86 default = {
87 flush = true;
88 fua = true;
89 };
90 description = lib.mdDoc ''
91 Extra options for this export. See
92 {manpage}`nbd-server(5)`.
93 '';
94 };
95 };
96 });
97 };
98
99 listenAddress = mkOption {
100 type = with types; nullOr str;
101 description = lib.mdDoc "Address to listen on. If not specified, the server will listen on all interfaces.";
102 default = null;
103 example = "10.10.0.1";
104 };
105 };
106 };
107 };
108
109 config = mkIf cfg.server.enable {
110 assertions = [
111 {
112 assertion = !(cfg.server.exports ? "generic");
113 message = "services.nbd.server exports must not be named 'generic'";
114 }
115 ];
116
117 boot.kernelModules = [ "nbd" ];
118
119 systemd.services.nbd-server = {
120 after = [ "network-online.target" ];
121 before = [ "multi-user.target" ];
122 wantedBy = [ "multi-user.target" ];
123 serviceConfig = {
124 ExecStart = "${pkgs.nbd}/bin/nbd-server -C ${serverConfig}";
125 Type = "forking";
126
127 DeviceAllow = map (path: "${path} rw") allowedDevices;
128 BindPaths = boundPaths;
129
130 CapabilityBoundingSet = "";
131 DevicePolicy = "closed";
132 LockPersonality = true;
133 MemoryDenyWriteExecute = true;
134 NoNewPrivileges = true;
135 PrivateDevices = false;
136 PrivateMounts = true;
137 PrivateTmp = true;
138 PrivateUsers = true;
139 ProcSubset = "pid";
140 ProtectClock = true;
141 ProtectControlGroups = true;
142 ProtectHome = true;
143 ProtectHostname = true;
144 ProtectKernelLogs = true;
145 ProtectKernelModules = true;
146 ProtectKernelTunables = true;
147 ProtectProc = "noaccess";
148 ProtectSystem = "strict";
149 RestrictAddressFamilies = "AF_INET AF_INET6";
150 RestrictNamespaces = true;
151 RestrictRealtime = true;
152 RestrictSUIDSGID = true;
153 UMask = "0077";
154 };
155 };
156 };
157}