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