256 lines
10 KiB
Nix
256 lines
10 KiB
Nix
{ 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;
|
|
})
|
|
]);
|
|
}
|