1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 cfg = config.services.nebula;
10 enabledNetworks = lib.filterAttrs (n: v: v.enable) cfg.networks;
11
12 format = pkgs.formats.yaml { };
13
14 nameToId = netName: "nebula-${netName}";
15
16 resolveFinalPort =
17 netCfg:
18 if netCfg.listen.port == null then
19 if (netCfg.isLighthouse || netCfg.isRelay) then 4242 else 0
20 else
21 netCfg.listen.port;
22in
23{
24 # Interface
25
26 options = {
27 services.nebula = {
28 networks = lib.mkOption {
29 description = "Nebula network definitions.";
30 default = { };
31 type = lib.types.attrsOf (
32 lib.types.submodule {
33 options = {
34 enable = lib.mkOption {
35 type = lib.types.bool;
36 default = true;
37 description = "Enable or disable this network.";
38 };
39
40 package = lib.mkPackageOption pkgs "nebula" { };
41
42 ca = lib.mkOption {
43 type = lib.types.path;
44 description = "Path to the certificate authority certificate.";
45 example = "/etc/nebula/ca.crt";
46 };
47
48 cert = lib.mkOption {
49 type = lib.types.path;
50 description = "Path to the host certificate.";
51 example = "/etc/nebula/host.crt";
52 };
53
54 key = lib.mkOption {
55 type = lib.types.oneOf [
56 lib.types.nonEmptyStr
57 lib.types.path
58 ];
59 description = "Path or reference to the host key.";
60 example = "/etc/nebula/host.key";
61 };
62
63 staticHostMap = lib.mkOption {
64 type = lib.types.attrsOf (lib.types.listOf (lib.types.str));
65 default = { };
66 description = ''
67 The static host map defines a set of hosts with fixed IP addresses on the internet (or any network).
68 A host can have multiple fixed IP addresses defined here, and nebula will try each when establishing a tunnel.
69 '';
70 example = {
71 "192.168.100.1" = [ "100.64.22.11:4242" ];
72 };
73 };
74
75 isLighthouse = lib.mkOption {
76 type = lib.types.bool;
77 default = false;
78 description = "Whether this node is a lighthouse.";
79 };
80
81 isRelay = lib.mkOption {
82 type = lib.types.bool;
83 default = false;
84 description = "Whether this node is a relay.";
85 };
86
87 lighthouse.dns.enable = lib.mkOption {
88 type = lib.types.bool;
89 default = false;
90 description = "Whether this lighthouse node should serve DNS.";
91 };
92
93 lighthouse.dns.host = lib.mkOption {
94 type = lib.types.str;
95 default = "localhost";
96 description = ''
97 IP address on which nebula lighthouse should serve DNS.
98 'localhost' is a good default to ensure the service does not listen on public interfaces;
99 use a Nebula address like 10.0.0.5 to make DNS resolution available to nebula hosts only.
100 '';
101 };
102
103 lighthouse.dns.port = lib.mkOption {
104 type = lib.types.nullOr lib.types.port;
105 default = 5353;
106 description = "UDP port number for lighthouse DNS server.";
107 };
108
109 lighthouses = lib.mkOption {
110 type = lib.types.listOf lib.types.str;
111 default = [ ];
112 description = ''
113 List of IPs of lighthouse hosts this node should report to and query from. This should be empty on lighthouse
114 nodes. The IPs should be the lighthouse's Nebula IPs, not their external IPs.
115 '';
116 example = [ "192.168.100.1" ];
117 };
118
119 relays = lib.mkOption {
120 type = lib.types.listOf lib.types.str;
121 default = [ ];
122 description = ''
123 List of IPs of relays that this node should allow traffic from.
124 '';
125 example = [ "192.168.100.1" ];
126 };
127
128 listen.host = lib.mkOption {
129 type = lib.types.str;
130 default = "0.0.0.0";
131 description = "IP address to listen on.";
132 };
133
134 listen.port = lib.mkOption {
135 type = lib.types.nullOr lib.types.port;
136 default = null;
137 defaultText = lib.literalExpression ''
138 if (config.services.nebula.networks.''${name}.isLighthouse ||
139 config.services.nebula.networks.''${name}.isRelay) then
140 4242
141 else
142 0;
143 '';
144 description = "Port number to listen on.";
145 };
146
147 tun.disable = lib.mkOption {
148 type = lib.types.bool;
149 default = false;
150 description = ''
151 When tun is disabled, a lighthouse can be started without a local tun interface (and therefore without root).
152 '';
153 };
154
155 tun.device = lib.mkOption {
156 type = lib.types.nullOr lib.types.str;
157 default = null;
158 description = "Name of the tun device. Defaults to nebula.\${networkName}.";
159 };
160
161 firewall.outbound = lib.mkOption {
162 type = lib.types.listOf lib.types.attrs;
163 default = [ ];
164 description = "Firewall rules for outbound traffic.";
165 example = [
166 {
167 port = "any";
168 proto = "any";
169 host = "any";
170 }
171 ];
172 };
173
174 firewall.inbound = lib.mkOption {
175 type = lib.types.listOf lib.types.attrs;
176 default = [ ];
177 description = "Firewall rules for inbound traffic.";
178 example = [
179 {
180 port = "any";
181 proto = "any";
182 host = "any";
183 }
184 ];
185 };
186
187 settings = lib.mkOption {
188 type = format.type;
189 default = { };
190 description = ''
191 Nebula configuration. Refer to
192 <https://github.com/slackhq/nebula/blob/master/examples/config.yml>
193 for details on supported values.
194 '';
195 example = lib.literalExpression ''
196 {
197 lighthouse.interval = 15;
198 }
199 '';
200 };
201 };
202 }
203 );
204 };
205 };
206 };
207
208 # Implementation
209 config = lib.mkIf (enabledNetworks != { }) {
210 systemd.services = lib.mkMerge (
211 lib.mapAttrsToList (
212 netName: netCfg:
213 let
214 networkId = nameToId netName;
215 settings = lib.recursiveUpdate {
216 pki = {
217 ca = netCfg.ca;
218 cert = netCfg.cert;
219 key = netCfg.key;
220 };
221 static_host_map = netCfg.staticHostMap;
222 lighthouse = {
223 am_lighthouse = netCfg.isLighthouse;
224 hosts = netCfg.lighthouses;
225 serve_dns = netCfg.lighthouse.dns.enable;
226 dns.host = netCfg.lighthouse.dns.host;
227 dns.port = netCfg.lighthouse.dns.port;
228 };
229 relay = {
230 am_relay = netCfg.isRelay;
231 relays = netCfg.relays;
232 use_relays = true;
233 };
234 listen = {
235 host = netCfg.listen.host;
236 port = resolveFinalPort netCfg;
237 };
238 tun = {
239 disabled = netCfg.tun.disable;
240 dev = if (netCfg.tun.device != null) then netCfg.tun.device else "nebula.${netName}";
241 };
242 firewall = {
243 inbound = netCfg.firewall.inbound;
244 outbound = netCfg.firewall.outbound;
245 };
246 } netCfg.settings;
247 configFile = format.generate "nebula-config-${netName}.yml" (
248 lib.warnIf
249 ((settings.lighthouse.am_lighthouse || settings.relay.am_relay) && settings.listen.port == 0)
250 ''
251 Nebula network '${netName}' is configured as a lighthouse or relay, and its port is ${builtins.toString settings.listen.port}.
252 You will likely experience connectivity issues: https://nebula.defined.net/docs/config/listen/#listenport
253 ''
254 settings
255 );
256 capabilities =
257 let
258 nebulaPort = if !settings.tun.disabled then settings.listen.port else 0;
259 dnsPort = if settings.lighthouse.serve_dns then settings.lighthouse.dns.port else 0;
260 in
261 lib.concatStringsSep " " (
262 # creation of tunnel interfaces
263 lib.optional (!settings.tun.disabled) "CAP_NET_ADMIN"
264 # binding to privileged ports
265 ++ lib.optional (
266 nebulaPort > 0 && nebulaPort < 1024 || dnsPort > 0 && dnsPort < 1024
267 ) "CAP_NET_BIND_SERVICE"
268 );
269 in
270 {
271 # Create the systemd service for Nebula.
272 "nebula@${netName}" = {
273 description = "Nebula VPN service for ${netName}";
274 wants = [ "basic.target" ];
275 after = [
276 "basic.target"
277 "network.target"
278 ];
279 before = [ "sshd.service" ];
280 wantedBy = [ "multi-user.target" ];
281 serviceConfig = {
282 Type = "notify";
283 Restart = "always";
284 ExecStart = "${netCfg.package}/bin/nebula -config ${configFile}";
285 UMask = "0027";
286 CapabilityBoundingSet = capabilities;
287 AmbientCapabilities = capabilities;
288 LockPersonality = true;
289 NoNewPrivileges = true;
290 PrivateDevices = false; # needs access to /dev/net/tun (below)
291 DeviceAllow = "/dev/net/tun rw";
292 DevicePolicy = "closed";
293 PrivateTmp = true;
294 PrivateUsers = false; # CapabilityBoundingSet needs to apply to the host namespace
295 ProtectClock = true;
296 ProtectControlGroups = true;
297 ProtectHome = true;
298 ProtectHostname = true;
299 ProtectKernelLogs = true;
300 ProtectKernelModules = true;
301 ProtectKernelTunables = true;
302 ProtectProc = "invisible";
303 ProtectSystem = true;
304 RestrictNamespaces = true;
305 RestrictSUIDSGID = true;
306 User = networkId;
307 Group = networkId;
308 };
309 unitConfig.StartLimitIntervalSec = 0; # ensure Restart=always is always honoured (networks can go down for arbitrarily long)
310 };
311 }
312 ) enabledNetworks
313 );
314
315 # Open the chosen ports for UDP.
316 networking.firewall.allowedUDPPorts = lib.unique (
317 lib.filter (port: port > 0) (
318 lib.mapAttrsToList (netName: netCfg: resolveFinalPort netCfg) enabledNetworks
319 )
320 );
321
322 # Create the service users and groups.
323 users.users = lib.mkMerge (
324 lib.mapAttrsToList (netName: netCfg: {
325 ${nameToId netName} = {
326 group = nameToId netName;
327 description = "Nebula service user for network ${netName}";
328 isSystemUser = true;
329 };
330 }) enabledNetworks
331 );
332
333 users.groups = lib.mkMerge (
334 lib.mapAttrsToList (netName: netCfg: {
335 ${nameToId netName} = { };
336 }) enabledNetworks
337 );
338 };
339
340 meta.maintainers = with lib.maintainers; [
341 numinit
342 siriobalmelli
343 ];
344}