main
1{lib, ...}: let
2 octets = ip: lib.splitString "." ip;
3 lastOctet = ip: lib.toInt (builtins.elemAt (octets ip) 3);
4 prefix = ip: lib.concatStringsSep "." (lib.take 3 (octets ip));
5 joinIP = prefix: last: "${prefix}.${toString last}";
6in {
7 den.aspects.services.kea = {
8 settings.host = {
9 interfaces = lib.mkOption {
10 description = "Interfaces the DHCP server listens on.";
11 type = lib.types.listOf lib.types.str;
12 };
13 staticRange = {
14 start = lib.mkOption {
15 description = "Start of the static IP range. Reservations must fall within it.";
16 type = lib.types.str;
17 default = "192.168.0.2";
18 };
19 end = lib.mkOption {
20 description = "End of the static IP range. Reservations must fall within it.";
21 type = lib.types.str;
22 default = "192.168.0.50";
23 };
24 };
25 poolEnd = lib.mkOption {
26 description = "End of the dynamic pool. The pool starts right after the static range.";
27 type = lib.types.str;
28 default = "192.168.0.200";
29 };
30 router = lib.mkOption {
31 description = "Default gateway advertised to clients.";
32 type = lib.types.nullOr lib.types.str;
33 default = null;
34 };
35 dnsServers = lib.mkOption {
36 description = "DNS servers advertised to clients.";
37 type = lib.types.listOf lib.types.str;
38 default = [];
39 };
40 extraReservations = lib.mkOption {
41 description = "Reservations for devices not managed by Nix, e.g. an AP.";
42 type = lib.types.listOf (lib.types.submodule {
43 options = {
44 hostname = lib.mkOption {
45 description = "Hostname for the device.";
46 type = lib.types.nullOr lib.types.str;
47 default = null;
48 };
49 mac = lib.mkOption {
50 description = "MAC address to reserve.";
51 type = lib.types.str;
52 };
53 ip = lib.mkOption {
54 description = "Fixed IP to assign.";
55 type = lib.types.str;
56 };
57 };
58 });
59 default = [];
60 };
61 };
62
63 nixos = {
64 host,
65 dhcpReservation,
66 ...
67 }: let
68 cfg = host.settings.services.kea;
69
70 fromHosts = lib.unique (
71 map (e: {
72 hostname = e.source.host.name;
73 mac = e.value.mac;
74 ip = e.value.ip;
75 })
76 dhcpReservation
77 );
78
79 reservations = cfg.extraReservations ++ fromHosts;
80
81 staticPrefix = prefix cfg.staticRange.start;
82 staticStart = lastOctet cfg.staticRange.start;
83 staticEnd = lastOctet cfg.staticRange.end;
84 poolStart = joinIP staticPrefix (staticEnd + 1);
85
86 inStaticRange = ip:
87 prefix ip
88 == staticPrefix
89 && lastOctet ip >= staticStart
90 && lastOctet ip <= staticEnd;
91
92 nameOf = r:
93 if r.hostname != null
94 then r.hostname
95 else r.ip;
96
97 macs = map (r: r.mac) reservations;
98 ips = map (r: r.ip) reservations;
99 in {
100 assertions =
101 (map (r: {
102 assertion = inStaticRange r.ip;
103 message =
104 "Kea reservation '${nameOf r}' has ip '${r.ip}' outside the static "
105 + "range '${cfg.staticRange.start} - ${cfg.staticRange.end}'";
106 })
107 reservations)
108 ++ [
109 {
110 assertion = lib.unique macs == macs;
111 message = "Kea reservations contain duplicate MAC addresses";
112 }
113 {
114 assertion = lib.unique ips == ips;
115 message = "Kea reservations contain duplicate IP addresses";
116 }
117 ];
118
119 services.kea.dhcp4 = {
120 enable = true;
121 settings = {
122 interfaces-config.interfaces = cfg.interfaces;
123 lease-database = {
124 name = "/var/lib/kea/dhcp4.leases";
125 persist = true;
126 type = "memfile";
127 };
128 valid-lifetime = 4000;
129 subnet4 = [
130 {
131 id = 1;
132 subnet = "${staticPrefix}.0/24";
133 pools = [{pool = "${poolStart} - ${cfg.poolEnd}";}];
134 reservations = map (r:
135 {
136 hw-address = r.mac;
137 ip-address = r.ip;
138 }
139 // lib.optionalAttrs (r.hostname != null) {hostname = r.hostname;})
140 reservations;
141 option-data =
142 lib.optional (cfg.router != null) {
143 name = "routers";
144 data = cfg.router;
145 }
146 ++ lib.optional (cfg.dnsServers != []) {
147 name = "domain-name-servers";
148 data = lib.concatStringsSep ", " cfg.dnsServers;
149 };
150 }
151 ];
152 };
153 };
154 };
155 };
156}