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 = "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 = "Enable or disable this network.";
28 };
29
30 package = mkOption {
31 type = types.package;
32 default = pkgs.nebula;
33 defaultText = "pkgs.nebula";
34 description = "Nebula derivation to use.";
35 };
36
37 ca = mkOption {
38 type = types.path;
39 description = "Path to the certificate authority certificate.";
40 example = "/etc/nebula/ca.crt";
41 };
42
43 cert = mkOption {
44 type = types.path;
45 description = "Path to the host certificate.";
46 example = "/etc/nebula/host.crt";
47 };
48
49 key = mkOption {
50 type = types.path;
51 description = "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 = ''
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 = literalExample ''
63 { "192.168.100.1" = [ "100.64.22.11:4242" ]; }
64 '';
65 };
66
67 isLighthouse = mkOption {
68 type = types.bool;
69 default = false;
70 description = "Whether this node is a lighthouse.";
71 };
72
73 lighthouses = mkOption {
74 type = types.listOf types.str;
75 default = [];
76 description = ''
77 List of IPs of lighthouse hosts this node should report to and query from. This should be empty on lighthouse
78 nodes. The IPs should be the lighthouse's Nebula IPs, not their external IPs.
79 '';
80 example = ''[ "192.168.100.1" ]'';
81 };
82
83 listen.host = mkOption {
84 type = types.str;
85 default = "0.0.0.0";
86 description = "IP address to listen on.";
87 };
88
89 listen.port = mkOption {
90 type = types.port;
91 default = 4242;
92 description = "Port number to listen on.";
93 };
94
95 tun.disable = mkOption {
96 type = types.bool;
97 default = false;
98 description = ''
99 When tun is disabled, a lighthouse can be started without a local tun interface (and therefore without root).
100 '';
101 };
102
103 tun.device = mkOption {
104 type = types.nullOr types.str;
105 default = null;
106 description = "Name of the tun device. Defaults to nebula.\${networkName}.";
107 };
108
109 firewall.outbound = mkOption {
110 type = types.listOf types.attrs;
111 default = [];
112 description = "Firewall rules for outbound traffic.";
113 example = ''[ { port = "any"; proto = "any"; host = "any"; } ]'';
114 };
115
116 firewall.inbound = mkOption {
117 type = types.listOf types.attrs;
118 default = [];
119 description = "Firewall rules for inbound traffic.";
120 example = ''[ { port = "any"; proto = "any"; host = "any"; } ]'';
121 };
122
123 settings = mkOption {
124 type = format.type;
125 default = {};
126 description = ''
127 Nebula configuration. Refer to
128 <link xlink:href="https://github.com/slackhq/nebula/blob/master/examples/config.yml"/>
129 for details on supported values.
130 '';
131 example = literalExample ''
132 {
133 lighthouse.dns = {
134 host = "0.0.0.0";
135 port = 53;
136 };
137 }
138 '';
139 };
140 };
141 });
142 };
143 };
144 };
145
146 # Implementation
147 config = mkIf (enabledNetworks != {}) {
148 systemd.services = mkMerge (mapAttrsToList (netName: netCfg:
149 let
150 networkId = nameToId netName;
151 settings = recursiveUpdate {
152 pki = {
153 ca = netCfg.ca;
154 cert = netCfg.cert;
155 key = netCfg.key;
156 };
157 static_host_map = netCfg.staticHostMap;
158 lighthouse = {
159 am_lighthouse = netCfg.isLighthouse;
160 hosts = netCfg.lighthouses;
161 };
162 listen = {
163 host = netCfg.listen.host;
164 port = netCfg.listen.port;
165 };
166 tun = {
167 disabled = netCfg.tun.disable;
168 dev = if (netCfg.tun.device != null) then netCfg.tun.device else "nebula.${netName}";
169 };
170 firewall = {
171 inbound = netCfg.firewall.inbound;
172 outbound = netCfg.firewall.outbound;
173 };
174 } netCfg.settings;
175 configFile = format.generate "nebula-config-${netName}.yml" settings;
176 in
177 {
178 # Create systemd service for Nebula.
179 "nebula@${netName}" = {
180 description = "Nebula VPN service for ${netName}";
181 wants = [ "basic.target" ];
182 after = [ "basic.target" "network.target" ];
183 before = [ "sshd.service" ];
184 wantedBy = [ "multi-user.target" ];
185 serviceConfig = mkMerge [
186 {
187 Type = "simple";
188 Restart = "always";
189 ExecStart = "${netCfg.package}/bin/nebula -config ${configFile}";
190 }
191 # The service needs to launch as root to access the tun device, if it's enabled.
192 (mkIf netCfg.tun.disable {
193 User = networkId;
194 Group = networkId;
195 })
196 ];
197 };
198 }) enabledNetworks);
199
200 # Open the chosen ports for UDP.
201 networking.firewall.allowedUDPPorts =
202 unique (mapAttrsToList (netName: netCfg: netCfg.listen.port) enabledNetworks);
203
204 # Create the service users and groups.
205 users.users = mkMerge (mapAttrsToList (netName: netCfg:
206 mkIf netCfg.tun.disable {
207 ${nameToId netName} = {
208 group = nameToId netName;
209 description = "Nebula service user for network ${netName}";
210 isSystemUser = true;
211 };
212 }) enabledNetworks);
213
214 users.groups = mkMerge (mapAttrsToList (netName: netCfg:
215 mkIf netCfg.tun.disable {
216 ${nameToId netName} = {};
217 }) enabledNetworks);
218 };
219}