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