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 "the Network Block Device (nbd) server";
47
48 listenPort = mkOption {
49 type = types.port;
50 default = 10809;
51 description = "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 = ''
60 Extra options for the server. See
61 {manpage}`nbd-server(5)`.
62 '';
63 };
64
65 exports = mkOption {
66 description = "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 = "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 = "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 = ''
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 = "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 wants = [ "network-online.target" ];
121 after = [ "network-online.target" ];
122 before = [ "multi-user.target" ];
123 wantedBy = [ "multi-user.target" ];
124 serviceConfig = {
125 ExecStart = "${pkgs.nbd}/bin/nbd-server -C ${serverConfig}";
126 Type = "forking";
127
128 DeviceAllow = map (path: "${path} rw") allowedDevices;
129 BindPaths = boundPaths;
130
131 CapabilityBoundingSet = "";
132 DevicePolicy = "closed";
133 LockPersonality = true;
134 MemoryDenyWriteExecute = true;
135 NoNewPrivileges = true;
136 PrivateDevices = false;
137 PrivateMounts = true;
138 PrivateTmp = true;
139 PrivateUsers = true;
140 ProcSubset = "pid";
141 ProtectClock = true;
142 ProtectControlGroups = true;
143 ProtectHome = true;
144 ProtectHostname = true;
145 ProtectKernelLogs = true;
146 ProtectKernelModules = true;
147 ProtectKernelTunables = true;
148 ProtectProc = "noaccess";
149 ProtectSystem = "strict";
150 RestrictAddressFamilies = "AF_INET AF_INET6";
151 RestrictNamespaces = true;
152 RestrictRealtime = true;
153 RestrictSUIDSGID = true;
154 UMask = "0077";
155 };
156 };
157 };
158}