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