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