nixos/nebula: Refactor module to allow for multiple nebula services on the same machine

This commit is contained in:
Tim Van Baak 2021-02-28 18:31:42 -08:00
parent 9f9e7c181c
commit 9f1ebd0c10

View File

@ -5,202 +5,219 @@ with lib;
let let
cfg = config.services.nebula; cfg = config.services.nebula;
nebulaDesc = "Nebula VPN service";
format = pkgs.formats.yaml {}; format = pkgs.formats.yaml {};
configFile = format.generate "nebula-config.yml" cfg.settings;
nameToId = netName: "nebula-${netName}";
in in
{ {
# Interface # Interface
options.services.nebula = { options = {
enable = mkEnableOption nebulaDesc; services.nebula = {
networks = mkOption {
description = "Nebula network definitions.";
default = {};
type = types.attrsOf (types.submodule {
options = {
package = mkOption {
type = types.package;
default = pkgs.nebula;
defaultText = "pkgs.nebula";
description = "Nebula derivation to use.";
};
package = mkOption { ca = mkOption {
type = types.package; type = types.path;
default = pkgs.nebula; description = "Path to the certificate authority certificate.";
defaultText = "pkgs.nebula"; example = "/etc/nebula/ca.crt";
description = "Nebula derivation to use."; };
};
ca = mkOption { cert = mkOption {
type = types.path; type = types.path;
description = "Path to the certificate authority certificate."; description = "Path to the host certificate.";
example = "/etc/nebula/ca.crt"; example = "/etc/nebula/host.crt";
}; };
cert = mkOption { key = mkOption {
type = types.path; type = types.path;
description = "Path to the host certificate."; description = "Path to the host key.";
example = "/etc/nebula/host.crt"; example = "/etc/nebula/host.key";
}; };
key = mkOption { staticHostMap = mkOption {
type = types.path; type = types.attrsOf (types.listOf (types.str));
description = "Path to the host key."; default = {};
example = "/etc/nebula/host.key"; description = ''
}; The static host map defines a set of hosts with fixed IP addresses on the internet (or any network).
A host can have multiple fixed IP addresses defined here, and nebula will try each when establishing a tunnel.
'';
example = literalExample ''
{ "192.168.100.1" = [ "100.64.22.11:4242" ]; }
'';
};
staticHostMap = mkOption { isLighthouse = mkOption {
type = types.attrsOf (types.listOf (types.str)); type = types.bool;
default = {}; default = false;
description = '' description = "Whether this node is a lighthouse.";
The static host map defines a set of hosts with fixed IP addresses on the internet (or any network). };
A host can have multiple fixed IP addresses defined here, and nebula will try each when establishing a tunnel.
'';
example = literalExample ''
{ "192.168.100.1" = [ "100.64.22.11:4242" ]; }
'';
};
isLighthouse = mkOption { lighthouses = mkOption {
type = types.bool; type = types.listOf types.str;
default = false; default = [];
description = "Whether this node is a lighthouse."; description = ''
}; List of IPs of lighthouse hosts this node should report to and query from. This should be empty on lighthouse
nodes. The IPs should be the lighthouse's Nebula IPs, not their external IPs.
'';
example = ''[ "192.168.100.1" ]'';
};
lighthouses = mkOption { listen.host = mkOption {
type = types.listOf types.str; type = types.str;
default = []; default = "0.0.0.0";
description = '' description = "IP address to listen on.";
List of IPs of lighthouse hosts this node should report to and query from. This should be empty on lighthouse };
nodes. The IPs should be the lighthouse's Nebula IPs, not their external IPs.
'';
example = ''[ "192.168.100.1" ]'';
};
listen.host = mkOption { listen.port = mkOption {
type = types.str; type = types.port;
default = "0.0.0.0"; default = 4242;
description = "IP address to listen on."; description = "Port number to listen on.";
}; };
listen.port = mkOption { punch = mkOption {
type = types.port; type = types.bool;
default = 4242; default = true;
description = "Port number to listen on."; description = ''
}; Continues to punch inbound/outbound at a regular interval to avoid expiration of firewall nat mappings.
'';
};
punch = mkOption { tun.disable = mkOption {
type = types.bool; type = types.bool;
default = true; default = false;
description = '' description = ''
Continues to punch inbound/outbound at a regular interval to avoid expiration of firewall nat mappings. When tun is disabled, a lighthouse can be started without a local tun interface (and therefore without root).
''; '';
}; };
tun.disable = mkOption { tun.device = mkOption {
type = types.bool; type = types.nullOr types.str;
default = false; default = null;
description = '' description = "Name of the tun device. Defaults to nebula.\${networkName}.";
When tun is disabled, a lighthouse can be started without a local tun interface (and therefore without root). };
'';
};
tun.device = mkOption { firewall.outbound = mkOption {
type = types.str; type = types.listOf types.attrs;
default = "nebula1"; default = [];
description = "Name of the tun device."; description = "Firewall rules for outbound traffic.";
}; example = ''[ { port = "any"; proto = "any"; host = "any"; } ]'';
};
firewall.outbound = mkOption { firewall.inbound = mkOption {
type = types.listOf types.attrs; type = types.listOf types.attrs;
default = []; default = [];
description = "Firewall rules for outbound traffic."; description = "Firewall rules for inbound traffic.";
example = ''[ { port = "any"; proto = "any"; host = "any"; } ]''; example = ''[ { port = "any"; proto = "any"; host = "any"; } ]'';
}; };
firewall.inbound = mkOption { settings = mkOption {
type = types.listOf types.attrs; type = format.type;
default = []; default = {};
description = "Firewall rules for inbound traffic."; description = ''
example = ''[ { port = "any"; proto = "any"; host = "any"; } ]''; Nebula configuration. Refer to
}; <link xlink:href="https://github.com/slackhq/nebula/blob/master/examples/config.yml"/>
for details on supported values.
settings = { '';
type = format.type; example = literalExample ''
default = {}; {
description = '' lighthouse.dns = {
Nebula configuration. Refer to host = "0.0.0.0";
<link xlink:href="https://github.com/slackhq/nebula/blob/master/examples/config.yml"/> port = 53;
for details on supported values. };
''; }
example = literalExample '' '';
{ };
lighthouse.dns = {
host = "0.0.0.0";
port = 53;
}; };
} });
''; };
}; };
}; };
# Implementation # Implementation
config = mkIf (cfg.networks != {}) {
config = mkIf cfg.enable { systemd.services = mkMerge (lib.mapAttrsToList (netName: netCfg:
services.nebula.settings = { let
pki = { networkId = nameToId netName;
ca = cfg.ca; settings = lib.recursiveUpdate {
cert = cfg.cert; pki = {
key = cfg.key; ca = netCfg.ca;
}; cert = netCfg.cert;
static_host_map = cfg.staticHostMap; key = netCfg.key;
lighthouse = { };
am_lighthouse = cfg.isLighthouse; static_host_map = netCfg.staticHostMap;
hosts = cfg.lighthouses; lighthouse = {
}; am_lighthouse = netCfg.isLighthouse;
listen = { hosts = netCfg.lighthouses;
host = cfg.listen.host; };
port = cfg.listen.port; listen = {
}; host = netCfg.listen.host;
punchy = { port = netCfg.listen.port;
punch = cfg.punch; };
}; punchy = {
tun = { punch = netCfg.punch;
disabled = cfg.tun.disable; };
dev = cfg.tun.device; tun = {
}; disabled = netCfg.tun.disable;
firewall = { dev = if (netCfg.tun.device != null) then netCfg.tun.device else "nebula.${netName}";
inbound = cfg.firewall.inbound; };
outbound = cfg.firewall.outbound; firewall = {
}; inbound = netCfg.firewall.inbound;
}; outbound = netCfg.firewall.outbound;
};
# Create systemd service for Nebula. } netCfg.settings;
systemd.services.nebula = { configFile = format.generate "nebula-config-${netName}.yml" settings;
description = nebulaDesc; in
after = [ "network.target" ];
before = [ "sshd.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = mkMerge [
{ {
Type = "simple"; # Create systemd service for Nebula.
Restart = "always"; "nebula@${netName}" = {
ExecStart = "${cfg.package}/bin/nebula -config ${configFile}"; description = "Nebula VPN service for ${netName}";
} after = [ "network.target" ];
# The service needs to launch as root to access the tun device, if it's enabled. before = [ "sshd.service" ];
(mkIf cfg.tun.disable { wantedBy = [ "multi-user.target" ];
User = "nebula"; serviceConfig = mkMerge [
Group = "nebula"; {
}) Type = "simple";
]; Restart = "always";
}; ExecStart = "${netCfg.package}/bin/nebula -config ${configFile}";
}
# The service needs to launch as root to access the tun device, if it's enabled.
(mkIf netCfg.tun.disable {
User = networkId;
Group = networkId;
})
];
};
}) cfg.networks);
# Open the chosen port for UDP. # Open the chosen ports for UDP.
networking.firewall.allowedUDPPorts = [ cfg.listen.port ]; networking.firewall.allowedUDPPorts =
lib.unique (lib.mapAttrsToList (netName: netCfg: netCfg.listen.port) cfg.networks);
# Create the service user and its group. # Create the service users and groups.
users = mkIf cfg.tun.disable { users.users = mkMerge (lib.mapAttrsToList (netName: netCfg:
users.nebula = { mkIf netCfg.tun.disable {
group = "nebula"; ${nameToId netName} = {
description = "Nebula service user"; group = nameToId netName;
isSystemUser = true; description = "Nebula service user for network ${netName}";
packages = [ cfg.package ]; isSystemUser = true;
}; packages = [ netCfg.package ];
};
}) cfg.networks);
groups.nebula = {}; users.groups = mkMerge (lib.mapAttrsToList (netName: netCfg:
}; mkIf netCfg.tun.disable {
${nameToId netName} = {};
}) cfg.networks);
}; };
} }