dotfiles/system/modules/router/kea.nix

256 lines
10 KiB
Nix
Raw Normal View History

2023-06-20 15:11:01 +07:00
{ lib
, config
, pkgs
, utils
, ... }:
let
cfg = config.router;
# add x to last component of an ipv4
addToLastComp4 = x: split:
let
n0 = lib.last split;
nx = n0 + x;
n = if nx >= 255 then 254 else if nx < 2 then 2 else nx;
in
if x > 0 && n0 >= 255 then null
else if x < 0 && n0 < 2 then null
else lib.init split ++ [ n ];
# add x to last component of an ipv6
addToLastComp6 = x: split:
let
n0 = lib.last split;
nx = n0 + x;
n = if nx >= 65535 then 65534 else if nx <= 2 then 2 else nx;
in
if x > 0 && n0 >= 65535 then null
else if x < 0 && n0 < 2 then null
else lib.init split ++ [ n ];
# generate an integer of `total` bits with `set` most significant bits set
genMask = total: set:
parseBin (builtins.concatStringsSep "" (builtins.genList (i: if i < set then "1" else "0") total));
# generate subnet mask for ipv4
genMask4 = len:
builtins.genList (i: let
len' = len - i * 8;
in
if len' <= 0 then 0
else if len' >= 8 then 255
else genMask 8 len') 4;
# generate subnet mask for ipv6
genMask6 = len:
builtins.genList (i: let
len' = len - i * 16;
in
if len' <= 0 then 0
else if len' >= 16 then 65535
else genMask 16 len') 8;
# invert a mask
invMask4 = map (builtins.bitXor 255);
invMask6 = map (builtins.bitXor 65535);
orMask = lib.zipListsWith builtins.bitOr;
andMask = lib.zipListsWith builtins.bitAnd;
# parses hexadecimal number
parseHex = x: (builtins.fromTOML "x=0x${x}").x;
# parses binary number
parseBin = x: (builtins.fromTOML "x=0b${x}").x;
# finds the longest zero-only sequence
# returns an attrset with maxS (start of the sequence) and max (sequence length)
longestZeroSeq =
builtins.foldl' ({ cur, max, curS, maxS, i }: elem: let self = {
i = i + 1;
cur = if elem == 0 then cur + 1 else 0;
max = if max >= self.cur then max else self.cur;
curS = if self.cur > 0 && cur > 0 then curS else if self.cur > 0 then i else -1;
maxS = if max >= self.cur then maxS else self.curS;
}; in self) { cur = 0; max = 0; curS = -1; maxS = -1; i = 0; };
# parses an IPv4 address
parseIp4 = x: map builtins.fromJSON (lib.splitString "." x);
# serializes an IPv4 address
compIp4 = x: builtins.concatStringsSep "." (map toString x);
# parses an IPv6 address
parseIp6 = x:
let
nzparts = map (x: if x == "" then [] else map parseHex (lib.splitString ":" x)) (lib.splitString "::" x);
in
if builtins.length nzparts == 1 then builtins.head nzparts
else let a = builtins.head nzparts; b = builtins.elemAt nzparts 1; in
a ++ (builtins.genList (_: 0) (8 - builtins.length a - builtins.length b)) ++ b;
# serializes an IPv6 address
compIp6 = x:
let
long = longestZeroSeq x;
joined = builtins.concatStringsSep ":" (builtins.foldl' ({ i, res }: x: {
i = i + 1;
res = res ++ (if i >= long.maxS && i < long.maxS + long.max then [ "" ] else [ (lib.toLower (lib.toHexString x)) ]);
}) { i = 0; res = [ ]; } x).res;
fix = builtins.replaceStrings [":::"] ["::"];
in
fix (fix (fix (fix (fix joined))));
format = pkgs.formats.json {};
package = pkgs.kea;
commonServiceConfig = {
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
DynamicUser = true;
User = "kea";
ConfigurationDirectory = "kea";
RuntimeDirectory = "kea";
StateDirectory = "kea";
UMask = "0077";
};
in {
config = lib.mkIf cfg.enable (lib.mkMerge [
(let
configs = builtins.mapAttrs (interface: icfg:
let
escapedInterface = utils.escapeSystemdPath interface;
cfg4 = icfg.ipv4.kea;
in if cfg4.configFile != null then cfg4.configFile else (format.generate "kea-dhcp4-${escapedInterface}.conf" {
Dhcp4 = {
valid-lifetime = 4000;
interfaces-config.interfaces = [ interface ];
lease-database = {
type = "memfile";
persist = true;
name = "/var/lib/kea/dhcp4-${escapedInterface}.leases";
};
subnet4 = map ({ address, prefixLength, gateways, dns, keaSettings, ... }:
let
subnetMask = genMask4 prefixLength;
parsed = parseIp4 address;
minIp = andMask subnetMask parsed;
maxIp = orMask (invMask4 subnetMask) parsed;
in {
subnet = "${address}/${toString prefixLength}";
option-data =
lib.optional (dns != [ ]) {
name = "domain-name-servers";
code = 6;
csv-format = true;
space = "dhcp4";
data = builtins.concatStringsSep ", " dns;
}
++ lib.optional (gateways != [ ]) {
name = "routers";
code = 3;
csv-format = true;
space = "dhcp4";
data = builtins.concatStringsSep ", " gateways;
};
pools = let
a = addToLastComp4 16 minIp;
b = addToLastComp4 (-16) parsed;
c = addToLastComp4 16 parsed;
d = addToLastComp4 (-16) maxIp;
in
lib.optional (a != null && b != null && a <= b) { pool = "${compIp4 a}-${compIp4 b}"; }
++ lib.optional (c != null && d != null && c <= d) { pool = "${compIp4 c}-${compIp4 d}"; };
} // keaSettings) icfg.ipv4.addresses;
} // cfg4.settings;
})) cfg.interfaces;
in {
environment.etc = lib.mapAttrs' (interface: icfg: {
name = "kea/dhcp4-server-${utils.escapeSystemdPath interface}.conf";
value = lib.mkIf (icfg.ipv4.kea.enable && icfg.ipv4.addresses != [ ]) {
source = configs.${interface};
};
}) cfg.interfaces;
systemd.services = lib.mapAttrs' (interface: icfg: let
escapedInterface = utils.escapeSystemdPath interface;
in {
name = "kea-dhcp4-server-${escapedInterface}";
value = lib.mkIf (icfg.ipv4.kea.enable && icfg.ipv4.addresses != [ ]) {
description = "Kea DHCP4 Server (${interface})";
documentation = [ "man:kea-dhcp4(8)" "https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp4-srv.html" ];
after = [ "network-online.target" "time-sync.target" "sys-subsystem-net-devices-${escapedInterface}.device" ];
bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ];
wantedBy = [ "multi-user.target" ];
environment = { KEA_PIDFILE_DIR = "/run/kea"; KEA_LOCKFILE_DIR = "/run/kea"; };
restartTriggers = [ configs.${interface} ];
serviceConfig = {
ExecStart = "${package}/bin/kea-dhcp4 -c "
+ lib.escapeShellArgs ([ "/etc/kea/dhcp4-server-${escapedInterface}.conf" ]);
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" ];
} // commonServiceConfig;
};
}) cfg.interfaces;
})
(let
configs = builtins.mapAttrs (interface: icfg:
let
escapedInterface = utils.escapeSystemdPath interface;
cfg6 = icfg.ipv6.kea;
in if cfg6.configFile != null then cfg6.configFile else (format.generate "kea-dhcp6-${escapedInterface}.conf" {
Dhcp6 = {
valid-lifetime = 4000;
preferred-lifetime = 3000;
interfaces-config.interfaces = [ interface ];
lease-database = {
type = "memfile";
persist = true;
name = "/var/lib/kea/dhcp6-${escapedInterface}.leases";
};
subnet6 = map ({ address, prefixLength, dns, keaSettings, ... }:
let
subnetMask = genMask6 prefixLength;
parsed = parseIp6 address;
minIp = andMask subnetMask parsed;
maxIp = orMask (invMask6 subnetMask) parsed;
in {
option-data =
lib.optional (dns != [ ]) {
name = "dns-servers";
code = 23;
csv-format = true;
space = "dhcp6";
data = builtins.concatStringsSep ", " (map (x: if builtins.isString x then x else x.address) dns);
};
subnet = "${address}/${toString prefixLength}";
pools = let
a = addToLastComp6 16 minIp;
b = addToLastComp6 (-16) parsed;
c = addToLastComp6 16 parsed;
d = addToLastComp6 (-16) maxIp;
in
lib.optional (a != null && b != null && a <= b) {
pool = "${compIp6 a}-${compIp6 b}";
} ++ lib.optional (c != null && d != null && c <= d) {
pool = "${compIp6 c}-${compIp6 d}";
};
} // keaSettings) icfg.ipv6.addresses;
} // cfg6.settings;
})) cfg.interfaces;
in {
environment.etc = lib.mapAttrs' (interface: icfg: {
name = "kea/dhcp6-server-${utils.escapeSystemdPath interface}.conf";
value = lib.mkIf (icfg.ipv6.kea.enable && icfg.ipv6.addresses != [ ]) {
source = configs.${interface};
};
}) cfg.interfaces;
systemd.services = lib.mapAttrs' (interface: icfg: let
escapedInterface = utils.escapeSystemdPath interface;
in {
name = "kea-dhcp6-server-${escapedInterface}";
value = lib.mkIf (icfg.ipv6.kea.enable && icfg.ipv6.addresses != [ ]) {
description = "Kea DHCP6 Server (${interface})";
documentation = [ "man:kea-dhcp6(8)" "https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp6-srv.html" ];
after = [ "network-online.target" "time-sync.target" "sys-subsystem-net-devices-${escapedInterface}.device" ];
bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ];
wantedBy = [ "multi-user.target" ];
environment = { KEA_PIDFILE_DIR = "/run/kea"; KEA_LOCKFILE_DIR = "/run/kea"; };
restartTriggers = [ configs.${interface} ];
serviceConfig = {
ExecStart = "${package}/bin/kea-dhcp6 -c "
+ lib.escapeShellArgs ([ "/etc/kea/dhcp6-server-${escapedInterface}.conf" ]);
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" ];
} // commonServiceConfig;
};
}) cfg.interfaces;
})
]);
}