network/nix/lib/config/options.nix

793 lines
24 KiB
Nix
Raw Normal View History

{ config, options, lib, ... }:
with lib;
let
# A host needs to know a network if it
# - is a configured interface, or
# - is required behind at least two different links
getHostLinkNetworks = hostName: link:
2021-11-03 01:07:44 +01:00
let
hostConfig = config.site.hosts.${hostName};
# all the host's links
hostLinkNetworks = builtins.mapAttrs (link: _:
networksBehindLink hostName link
) hostConfig.links;
# how many links have a net in networksBehindLink
networkLinkCount = net:
builtins.length (
builtins.filter (builtins.elem net)
(builtins.attrValues hostLinkNetworks)
);
2021-11-03 01:07:44 +01:00
in
# access port
if config.site.net ? ${link}
2021-11-03 01:07:44 +01:00
then [ link ]
# multiple vlans on this link
else
builtins.filter (net:
# this port and local interface
hostConfig.interfaces ? ${net}
||
# this port and another
networkLinkCount net > 1
) hostLinkNetworks.${link};
2021-11-03 01:07:44 +01:00
networksBehindLink = hostName: name:
networksBehindLink' {
"${hostName}" = true;
} [ name ];
2021-11-03 01:07:44 +01:00
networksBehindLink' = seen: links:
2021-11-03 01:07:44 +01:00
if links == []
then
# Done, result is the seen link names that are networks:
builtins.filter (name:
config.site.net ? ${name}
) (builtins.attrNames seen)
2021-11-03 01:07:44 +01:00
else
let
link = builtins.head links;
seen' = seen // {
"${link}" = true;
};
onlyUnseen = builtins.filter (link: ! seen' ? ${link});
2021-11-03 01:07:44 +01:00
links' = builtins.tail links;
in
if config.site.hosts ? ${link}
2022-01-18 01:05:16 +01:00
then networksBehindLink' seen' (builtins.filter (link: ! seen' ? ${link}) (
links' ++ (
builtins.attrNames config.site.hosts.${link}.interfaces
) ++ (
onlyUnseen (builtins.attrNames config.site.hosts.${link}.links)
)
2022-01-18 01:05:16 +01:00
))
else if config.site.net ? ${link}
then networksBehindLink' seen' links'
else throw "Link to invalid target ${link}";
2021-11-03 01:07:44 +01:00
dhcpOpts = {
start = mkOption {
description = "First IP address in pool";
type = types.str;
};
end = mkOption {
description = "Last IP address in pool";
type = types.str;
};
time = mkOption {
description = "Renew time in seconds";
type = types.int;
};
max-time = mkOption {
description = "Max renew time in seconds";
type = types.int;
};
server = mkOption {
description = "Container that runs the DHCP server";
type = types.str;
};
router = mkOption {
description = "Gateway";
type = types.str;
};
2021-03-31 02:20:08 +02:00
fixed-hosts = mkOption {
type = with types; attrsOf str;
default = {};
};
};
2021-09-19 02:18:17 +02:00
netOpts = { name, ... }: {
options = {
vlan = mkOption {
description = "VLAN tag number";
2022-01-18 01:05:16 +01:00
type = with types; nullOr int;
};
subnet4 = mkOption {
description = "v.w.x.y/z";
2021-02-25 01:06:32 +01:00
type = with types; nullOr str;
default = null;
};
subnet4Net = mkOption {
type = with types; nullOr types.str;
default =
let
inherit (config.site.net.${name}) subnet4;
s = lib.splitString "/" subnet4;
in
if subnet4 != null && builtins.length s == 2
then builtins.head s
else null;
description = "Don't set, use subnet4 instead!";
};
subnet4Len = mkOption {
type = with types; nullOr types.int;
default =
let
inherit (config.site.net.${name}) subnet4;
s = lib.splitString "/" subnet4;
in
if subnet4 != null && builtins.length s == 2
then lib.toInt (elemAt s 1)
else null;
description = "Don't set, use subnet4 instead!";
};
subnets6 = mkOption {
description = "IPv6 subnets w/o prefixlen (always 64)";
2021-02-25 01:06:32 +01:00
type = with types; attrsOf str;
default = {};
};
2021-03-25 00:08:24 +01:00
hosts4 = mkOption {
description = "Attribute set of hostnames to IPv4 addresses";
type = with types; attrsOf str;
default = {};
};
hosts6 = mkOption {
description = "Attribute set of contexts to attribute sets of hostnames to IPv4 addresses";
type = with types; attrsOf (attrsOf str);
default = {};
};
ospf = {
secret = mkOption {
type = with types; nullOr str;
default = null;
};
};
dhcp = mkOption {
type = with types; nullOr (submodule { options = dhcpOpts; });
default = null;
};
ipv6Router = mkOption {
description = "Who sends router advertisements?";
type = with types; nullOr str;
default = config.site.net.${name}.dhcp.router or null;
};
2021-04-02 03:09:45 +02:00
domainName = mkOption {
description = "Domain name option";
type = types.str;
2021-11-10 20:12:45 +01:00
default = "${name}.zentralwerk.org";
};
extraRecords = mkOption {
type = with types; listOf (submodule {
options = {
name = mkOption {
type = str;
};
type = mkOption {
type = enum [ "A" "AAAA" "MX" "SRV" "CNAME" "TXT" ];
};
data = mkOption {
type = str;
};
};
});
default = [];
description = "Extraneous DNS records";
2021-05-03 01:26:57 +02:00
};
dynamicDomain = mkOption {
type = types.bool;
default = false;
description = "Domain updated by DHCP server?";
2021-04-02 03:09:45 +02:00
};
2022-01-18 01:05:16 +01:00
mtu = mkOption {
type = with types; nullOr int;
default = null;
};
};
};
2021-09-19 02:18:17 +02:00
upstreamOpts = {
provider = mkOption {
type = types.str;
};
link = mkOption {
type = with types; nullOr str;
default = null;
description = "Underlying interface name for eg. PPPoE";
};
staticIpv4Address = mkOption {
type = with types; nullOr str;
default = null;
};
upBandwidth = mkOption {
type = with types; nullOr int;
default = null;
};
noNat.subnets4 = mkOption {
type = with types; listOf str;
default = [];
description = "Do not NAT traffic from these public static subnets";
};
noNat.subnets6 = mkOption {
type = with types; listOf str;
default = [];
description = "Do not NAT66 traffic from these public static subnets";
};
2021-11-13 01:23:23 +01:00
user = mkOption {
type = with types; nullOr str;
default = null;
};
password = mkOption {
type = with types; nullOr str;
default = null;
};
};
2021-09-19 02:18:17 +02:00
2022-03-22 18:13:17 +01:00
interfaceOpts = { ... }: {
2021-03-23 00:40:40 +01:00
options = {
hwaddr = mkOption {
type = with types; nullOr str;
default = null;
2021-04-10 14:52:13 +02:00
description = "Static MAC address";
2021-03-23 00:40:40 +01:00
};
type = mkOption {
2022-01-18 01:05:16 +01:00
type = types.enum [ "phys" "veth" "pppoe" "bridge" "wireguard" "vxlan" ];
2021-04-10 14:52:13 +02:00
description = ''
veth: Virtual ethernet to be attached to a bridge.
phys: (Physical) interface from a server moved into the
container. Do not use with VLAN interfaces because they
won't be moved back after lxc-stop.
'';
2021-03-23 00:40:40 +01:00
};
2021-03-25 00:08:24 +01:00
gw4 = mkOption {
2021-03-23 00:40:40 +01:00
type = with types; nullOr str;
default = null;
2021-04-10 14:52:13 +02:00
description = "IPv4 gateway";
2021-03-23 00:40:40 +01:00
};
gw6 = mkOption {
type = with types; nullOr str;
default = null;
2021-04-10 14:52:13 +02:00
description = "IPv6 gateway";
2021-03-23 00:40:40 +01:00
};
upstream = mkOption {
type = with types; nullOr (submodule { options = upstreamOpts; });
default = null;
2021-04-10 14:52:13 +02:00
description = "Upstream interface configuration";
};
wireguard = mkOption {
default = null;
type = with types; nullOr (submodule {
options = {
endpoint = mkOption {
type = str;
};
publicKey = mkOption {
type = str;
};
privateKey = mkOption {
type = str;
};
addresses = mkOption {
type = listOf str;
};
};
});
};
2022-01-18 01:05:16 +01:00
vxlan = mkOption {
default = null;
type = with types; nullOr (submodule {
options = {
peer = mkOption {
type = str;
};
};
});
};
2021-03-23 00:40:40 +01:00
};
};
2021-09-19 02:18:17 +02:00
2021-02-25 01:06:32 +01:00
hostOpts = { name, ... }: {
options = {
prebuilt = mkOption {
type = types.bool;
default = false;
description = ''
Include the container system in the server's `build-container` script.
'';
};
firstboot = mkOption {
type = types.bool;
default = false;
description = ''
true if host is a newly flashed OpenWRT device with a default address
'';
};
2021-02-25 01:06:32 +01:00
role = mkOption {
2021-03-20 00:06:31 +01:00
type = types.enum [ "ap" "switch" "server" "container" "client" ];
2021-02-25 01:06:32 +01:00
default = "client";
};
model = mkOption {
type = types.str;
2021-03-20 00:06:31 +01:00
default = {
ap = "unknown";
switch = "unknown";
server = "pc";
container = "lxc";
client = "any";
}."${config.site.hosts.${name}.role}";
2021-02-25 01:06:32 +01:00
};
password = mkOption {
type = with types; nullOr str;
default = null;
};
location = mkOption {
type = with types; nullOr str;
default = null;
};
2021-03-23 00:40:40 +01:00
interfaces = mkOption {
default = {};
type = with types; attrsOf (submodule interfaceOpts);
2021-04-10 14:52:13 +02:00
description = "Network interfaces";
2021-03-23 00:40:40 +01:00
};
2021-05-31 00:06:56 +02:00
physicalInterfaces = mkOption {
default = lib.filterAttrs (_: { type, ... }:
builtins.elem type [ "phys" "veth" ]
) config.site.hosts.${name}.interfaces;
type = with types; attrsOf (submodule interfaceOpts);
description = "Network interfaces that are not virtual (don't set!)";
};
isRouter = mkOption {
type = types.bool;
# isRouter = Part of the core network?
default =
config.site.hosts.${name}.interfaces ? core &&
config.site.net.core.hosts4 ? ${name} &&
config.site.hosts.${name}.role == "container";
description = "Should this host route?";
};
firewall.enable = mkOption {
type = types.bool;
default = false;
description = "Enable firewall to disallow incoming connections from core";
};
2021-04-08 02:30:50 +02:00
forwardPorts = mkOption {
type = with types; listOf (submodule { options = {
proto = mkOption {
type = types.enum [ "tcp" "udp" ];
};
sourcePort = mkOption {
type = types.int;
};
destination = mkOption {
type = types.str;
};
reflect = mkOption {
type = types.bool;
default = true;
description = ''
Enable NAT reflection
Any forwarded connection will have our static IPv4
address as source so that forwarded services become
available internally.
Unfortunately, this breaks identification by IPv4
adress.
'';
};
}; });
default = [];
};
ospf.stubNets4 = mkOption {
type = with types; listOf str;
default = [];
2021-04-10 14:52:13 +02:00
description = "Additional IPv4 networks to announce";
};
ospf.stubNets6 = mkOption {
type = with types; listOf str;
default = [];
2021-04-10 14:52:13 +02:00
description = "Additional IPv6 networks to announce";
};
ospf.allowedUpstreams = mkOption {
type = with types; listOf str;
default = [];
description = "Accept default routes from these OSPF routers, in order of preference";
};
2022-09-18 01:37:57 +02:00
ospf.allowedUpstreams6 = mkOption {
type = with types; listOf str;
default = config.site.hosts.${name}.ospf.allowedUpstreams;
description = "Accept IPv6 default routes from these OSPF3 routers, in order of preference";
};
ospf.upstreamInstance = mkOption {
type = with types; nullOr int;
default = null;
description = "OSPF instance for advertising the default route";
};
2021-04-13 00:11:42 +02:00
bgp = mkOption {
default = null;
type = with types; nullOr (submodule {
options = bgpOpts;
});
};
2021-05-03 01:26:57 +02:00
services.dns = {
enable = mkOption {
type = types.bool;
default = false;
};
};
2021-04-14 23:07:27 +02:00
services.dnscache.enable = mkOption {
type = types.bool;
default = false;
};
2022-01-13 23:40:43 +01:00
services.yggdrasil = {
enable = mkOption {
type = types.bool;
default = false;
};
keys = mkOption {
type = types.str;
default = "";
};
};
2021-11-03 01:07:44 +01:00
links = mkOption {
description = "Which port is connected to what other device? Keys are either network names or known hostnames.";
default = {};
type = with types; attrsOf (submodule (linkOpts name));
2021-11-03 01:07:44 +01:00
};
wifi = mkOption {
default = {};
type = with types; attrsOf (submodule (
2022-03-22 18:13:17 +01:00
{ ... }: {
options = {
htmode = mkOption {
type = enum [ "HT20" "HT40-" "HT40+" "VHT80" ];
};
channel = mkOption {
type = int;
};
ssids = mkOption {
type = attrsOf (submodule ({ config, ... }: {
options = {
net = mkOption {
type = str;
};
psk = mkOption {
type = nullOr str;
default = null;
};
encryption = mkOption {
type = enum [ "none" "owe" "wpa2" "wpa3" ];
default =
if config.psk == null
then "none"
else "wpa3";
};
mode = mkOption {
type = enum [ "ap" "sta" ];
default = "ap";
};
ifname = mkOption {
type = nullOr str;
default = null;
};
};
}));
};
};
}
));
};
wifiOnLink.enable = mkOption {
type = types.bool;
default = true;
description = ''
Install the wifi-on-link.sh script on OpenWRT devices.
'';
};
2021-04-13 00:11:42 +02:00
};
};
2021-09-19 02:18:17 +02:00
2021-04-13 00:11:42 +02:00
bgpOpts = {
asn = mkOption {
type = types.int;
};
peers = mkOption {
2022-03-22 18:13:17 +01:00
type = with types; attrsOf (submodule ({ ... }: {
options = {
asn = mkOption {
type = types.int;
};
};
}));
2021-04-13 00:11:42 +02:00
default = {};
2021-02-25 01:06:32 +01:00
};
};
2021-11-03 01:07:44 +01:00
linkOpts = hostName: { name, ... }: {
2021-11-03 01:07:44 +01:00
options = {
ports = mkOption {
type = with types; listOf str;
description = "Port names";
};
group = mkOption {
type = with types; nullOr str;
default = null;
description = "Link aggregation group with a fixed number";
};
nets = mkOption {
type = with types; listOf str;
description = "Automatically generated";
default = getHostLinkNetworks hostName name;
2021-11-03 01:07:44 +01:00
};
vlans = mkOption {
type = with types; listOf int;
description = "Automatically generated, do not set";
2022-01-18 01:05:16 +01:00
default = builtins.concatMap (net:
let
inherit (config.site.net.${net}) vlan;
in if vlan != null
then [ vlan ]
else []
) config.site.hosts.${hostName}.links.${name}.nets;
2021-11-03 01:07:44 +01:00
};
2021-11-06 22:56:54 +01:00
trunk = mkOption {
type = types.bool;
description = "Trunk with tagged VLANs?";
default =
if config.site.net ? ${name}
then false
else if config.site.hosts ? ${name}
then true
else throw "Invalid link target: \"${name}\"";
};
2021-11-03 01:07:44 +01:00
};
};
2022-03-01 22:51:31 +01:00
vpnOpts = {
privateKey = mkOption {
type = types.str;
};
port = mkOption {
type = types.int;
default = 1337;
};
peers = mkOption {
type = with types; listOf (submodule {
options = {
publicKey = mkOption {
type = str;
};
allowedIPs = mkOption {
type = listOf str;
};
};
});
};
};
in
{
options.site = {
net = mkOption {
2021-09-19 02:18:17 +02:00
description = "All subnets";
default = {};
type = with types; attrsOf (submodule netOpts);
};
2021-11-04 19:17:57 +01:00
2022-12-20 03:47:27 +01:00
net-combined = mkOption {
description = "All hosts of all subnets";
default = {};
type = with types; submodule netOpts;
};
2021-02-25 01:06:32 +01:00
hosts = mkOption {
2021-09-19 02:18:17 +02:00
description = "All the static hosts";
2021-02-25 01:06:32 +01:00
default = {};
type = with types; attrsOf (submodule hostOpts);
};
2021-11-04 19:17:57 +01:00
sshPubKeys = mkOption {
type = with types; listOf str;
};
2021-11-13 01:23:23 +01:00
dyndnsKey = mkOption {
type = types.str;
};
2022-03-01 22:51:31 +01:00
vpn.wireguard = vpnOpts;
mqttServer = {
host = mkOption {
type = types.str;
default = config.site.net.serv.hosts4.broker;
};
user = mkOption {
type = types.str;
default = "user";
};
password = mkOption {
type = types.str;
default = "secret";
};
};
};
config.warnings =
let
findCollisions = getter: xs:
(builtins.foldl' ({ known, dup }: k:
let
ks = builtins.toString k;
in
if known ? ${ks}
then { inherit known; dup = dup ++ [ks]; }
else { known = known // { ${ks} = true; }; inherit dup; }
) {
known = {}; dup = [];
} (
concatMap getter (builtins.attrValues xs)
)).dup;
reportCollisions = name: getter: xs:
map (k: "Duplicate ${name}: ${k}") (findCollisions getter xs);
ospfUpstreamXorGw =
builtins.concatMap (hostName:
let
hostConf = config.site.hosts.${hostName};
gwNets = builtins.filter (netName:
hostConf.interfaces.${netName}.gw4 != null
) (builtins.attrNames hostConf.interfaces);
in if gwNets != [] && hostConf.ospf.allowedUpstreams != []
then [ ''
Host ${hostName} has gateway on ${builtins.head gwNets} but accepts default routes from OSPF
'' ]
else []
) (builtins.attrNames config.site.hosts);
in
2022-03-01 22:51:31 +01:00
(reportCollisions "VLAN tag" (x: lib.optional (x.vlan != null) x.vlan) config.site.net) ++
(reportCollisions "IPv4 subnet" (x: if x.subnet4 == null then [] else [x.subnet4]) config.site.net) ++
(reportCollisions "IPv6 subnet" (x: builtins.attrValues x.subnets6) config.site.net) ++
ospfUpstreamXorGw;
config.assertions =
# Duplicate host/net name check
map (name: {
assertion = ! config.site.net ? ${name};
message = "Host \"${name}\" must be named differently if net \"${name}\" exists.";
}) (builtins.attrNames config.site.hosts)
++
# Duplicate address check
(let
addrHosts =
builtins.foldl' (result: { hosts4, ... }:
builtins.foldl' (result: host:
let
addr = hosts4.${host};
in
if result ? ${addr}
then result // {
"${addr}" = result.${addr} ++ [ host ];
}
else result // {
"${addr}" = [ host ];
}
) result (builtins.attrNames hosts4)
) {} (builtins.attrValues config.site.net)
//
builtins.foldl' (result: net:
builtins.foldl' (result: ctx:
builtins.foldl' (result: host:
let
addr = config.site.net.${net}.hosts6.${ctx}.${host};
in
if result ? ${addr}
then result // {
"${addr}" = result.${addr} ++ [ host ];
}
else result // {
"${addr}" = [ host ];
}
) result (builtins.attrNames config.site.net.${net}.hosts6.${ctx})
) result (builtins.attrNames config.site.net.${net}.hosts6)
) {} (builtins.attrNames config.site.net);
in map (addr: {
assertion = builtins.length addrHosts.${addr} == 1;
message = "Address ${addr} is assigned to more than one host: ${lib.concatStringsSep " " addrHosts.${addr}}";
}) (builtins.attrNames addrHosts))
++
# duplicate vlan check
(let
vlanNets =
builtins.foldl' (result: net:
let
vlan = toString config.site.net.${net}.vlan;
in
2022-01-18 01:05:16 +01:00
if config.site.net.${net}.vlan != null && result ? ${vlan}
then result // {
"${vlan}" = result.${vlan} ++ [ net ];
}
else result // {
"${vlan}" = [ net ];
}
) {} (builtins.attrNames config.site.net);
in map (vlan: {
assertion = builtins.length vlanNets.${vlan} == 1;
message = "VLAN ${vlan} is used by more than one network: ${lib.concatStringsSep " " vlanNets.${vlan}}";
}) (builtins.attrNames vlanNets))
++
# Duplicate switch port check
builtins.concatMap (hostName:
let
ports = lib.unique (
builtins.concatMap (linkName:
config.site.hosts.${hostName}.links.${linkName}.ports
) (builtins.attrNames config.site.hosts.${hostName}.links)
);
linksOfPort = port:
builtins.attrNames (
lib.filterAttrs (_: { ports, ... }: builtins.elem port ports)
config.site.hosts.${hostName}.links
);
in map (port: {
assertion = builtins.length (linksOfPort port) == 1;
message = "${hostName}: port ${port} is used in more than one link: ${lib.concatStringsSep " " (linksOfPort port)}";
}) ports
) (builtins.attrNames config.site.hosts)
++
# Duplicate switch port group check
builtins.concatMap (hostName:
let
groups = lib.unique (
builtins.filter builtins.isString (
builtins.map (linkName:
config.site.hosts.${hostName}.links.${linkName}.group
) (builtins.attrNames config.site.hosts.${hostName}.links)
)
);
linksOfGroup = wantedGroup:
builtins.attrNames (
lib.filterAttrs (_: { group, ... }: group == wantedGroup)
config.site.hosts.${hostName}.links
);
in map (group: {
assertion = builtins.length (linksOfGroup group) == 1;
message = "${hostName}: group ${group} is used in more than one link: ${lib.concatStringsSep " " (linksOfGroup group)}";
}) groups
) (builtins.attrNames config.site.hosts)
++
# wifi psk checks
builtins.concatMap (hostName:
builtins.concatMap (wifiPath:
map (ssid:
let
ssidConf = config.site.hosts.${hostName}.wifi.${wifiPath}.ssids.${ssid};
in
if builtins.elem ssidConf.encryption [ "none" "owe" ]
then {
assertion = ssidConf.psk == null;
message = "${hostName}: SSID ${ssid} has encryption ${ssidConf.encryption} but a PSK is set";
}
else if builtins.elem ssidConf.encryption [ "wpa2" "wpa3" ]
then {
assertion = ssidConf.psk != null;
message = "${hostName}: SSID ${ssid} has encryption ${ssidConf.encryption} but no PSK is set";
}
else throw "Unsupported WiFi encryption ${ssidConf.encryption}"
) (builtins.attrNames config.site.hosts.${hostName}.wifi.${wifiPath}.ssids)
) (builtins.attrNames config.site.hosts.${hostName}.wifi)
) (builtins.attrNames config.site.hosts);
}