1{
2 description = "onis — decentralized DNS over ATProto";
3
4 inputs = {
5 nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
6 crane.url = "github:ipetkov/crane";
7 flake-utils.url = "github:numtide/flake-utils";
8 };
9
10 outputs = { self, nixpkgs, crane, flake-utils, ... }:
11 let
12 perSystem = flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
13 let
14 pkgs = import nixpkgs {
15 inherit system;
16 };
17
18 craneLib = crane.mkLib pkgs;
19
20 commonArgs = {
21 src = let
22 sqlFilter = path: _type: builtins.match ".*\.sql$" path != null;
23 sqlOrCargo = path: type:
24 (sqlFilter path type) || (craneLib.filterCargoSources path type);
25 in
26 pkgs.lib.cleanSourceWith {
27 src = ./.;
28 filter = sqlOrCargo;
29 };
30
31 buildInputs = with pkgs; [
32 openssl
33 ];
34
35 nativeBuildInputs = with pkgs; [
36 pkg-config
37 ];
38 };
39
40 cargoArtifacts = craneLib.buildDepsOnly commonArgs;
41
42 onis-appview = craneLib.buildPackage (commonArgs // {
43 inherit cargoArtifacts;
44 cargoExtraArgs = "--bin onis-appview";
45 });
46
47 onis-dns = craneLib.buildPackage (commonArgs // {
48 inherit cargoArtifacts;
49 cargoExtraArgs = "--bin onis-dns";
50 });
51
52 onis-verify = craneLib.buildPackage (commonArgs // {
53 inherit cargoArtifacts;
54 cargoExtraArgs = "--bin onis-verify";
55 });
56 in
57 {
58 packages = {
59 inherit onis-appview onis-dns onis-verify;
60 default = pkgs.symlinkJoin {
61 name = "onis";
62 paths = [ onis-appview onis-dns onis-verify ];
63 };
64 };
65
66 checks = {
67 onis-clippy = craneLib.cargoClippy (commonArgs // {
68 inherit cargoArtifacts;
69 cargoClippyExtraArgs = "-- --deny warnings";
70 });
71
72 onis-fmt = craneLib.cargoFmt {
73 src = commonArgs.src;
74 };
75
76 onis-test = craneLib.cargoNextest (commonArgs // {
77 inherit cargoArtifacts;
78 });
79 };
80
81 devShells.default = craneLib.devShell {
82 checks = self.checks.${system};
83
84 packages = with pkgs; [
85 clippy
86 rust-analyzer
87 sqlx-cli
88 cargo-watch
89 pkg-config
90 openssl
91 sqlite
92 dig
93 ];
94 };
95 }
96 );
97 in
98 perSystem // {
99
100 nixosModules.default = { config, lib, pkgs, ... }:
101 let
102 inherit (lib) mkEnableOption mkOption types mkIf mkMerge;
103 cfg = config.services.onis;
104 settingsFormat = pkgs.formats.toml { };
105
106 configFile = settingsFormat.generate "onis.toml" {
107 appview = {
108 inherit (cfg.appview) bind tap_url tap_acks tap_reconnect_delay index_path db_dir;
109 database = {
110 inherit (cfg.appview.database) busy_timeout user_max_connections index_max_connections;
111 };
112 };
113 dns = {
114 inherit (cfg.dns) appview_url bind port tcp_timeout ttl_floor slow_query_threshold_ms ns metrics_bind;
115 soa = {
116 inherit (cfg.dns.soa) ttl refresh retry expire minimum mname rname;
117 };
118 };
119 verify = {
120 inherit (cfg.verify) appview_url bind port check_interval recheck_interval expected_ns nameservers dns_port;
121 };
122 };
123 in
124 {
125 options.services.onis = {
126
127 # -----------------------------------------------------------------
128 # Appview
129 # -----------------------------------------------------------------
130 appview = {
131 enable = mkEnableOption "onis appview service";
132
133 package = mkOption {
134 type = types.package;
135 default = self.packages.${pkgs.system}.onis-appview;
136 defaultText = lib.literalExpression "self.packages.\${pkgs.system}.onis-appview";
137 description = "The onis-appview package to use.";
138 };
139
140 bind = mkOption {
141 type = types.str;
142 default = "localhost:3000";
143 description = "Address and port for the appview HTTP server.";
144 };
145
146 tap_url = mkOption {
147 type = types.str;
148 default = "ws://localhost:2480/channel";
149 description = "WebSocket URL for the TAP firehose.";
150 };
151
152 tap_acks = mkOption {
153 type = types.bool;
154 default = true;
155 description = "Whether to acknowledge TAP messages.";
156 };
157
158 tap_reconnect_delay = mkOption {
159 type = types.int;
160 default = 5;
161 description = "Seconds to wait before reconnecting after a TAP connection error.";
162 };
163
164 index_path = mkOption {
165 type = types.str;
166 default = "/var/lib/onis-appview/index.db";
167 description = "Path to the shared index SQLite database.";
168 };
169
170 db_dir = mkOption {
171 type = types.str;
172 default = "/var/lib/onis-appview/dbs";
173 description = "Directory for per-user SQLite databases.";
174 };
175
176 database = {
177 busy_timeout = mkOption {
178 type = types.int;
179 default = 5;
180 description = "Seconds to wait when the database is locked.";
181 };
182
183 user_max_connections = mkOption {
184 type = types.int;
185 default = 5;
186 description = "Max connections for per-user database pools.";
187 };
188
189 index_max_connections = mkOption {
190 type = types.int;
191 default = 10;
192 description = "Max connections for the shared index database pool.";
193 };
194 };
195 };
196
197 # -----------------------------------------------------------------
198 # DNS
199 # -----------------------------------------------------------------
200 dns = {
201 enable = mkEnableOption "onis DNS server";
202
203 package = mkOption {
204 type = types.package;
205 default = self.packages.${pkgs.system}.onis-dns;
206 defaultText = lib.literalExpression "self.packages.\${pkgs.system}.onis-dns";
207 description = "The onis-dns package to use.";
208 };
209
210 appview_url = mkOption {
211 type = types.str;
212 default = "http://localhost:3000";
213 description = "URL of the onis appview API.";
214 };
215
216 bind = mkOption {
217 type = types.str;
218 default = "0.0.0.0";
219 description = "Address for the DNS server to listen on.";
220 };
221
222 port = mkOption {
223 type = types.port;
224 default = 53;
225 description = "Port for the DNS server.";
226 };
227
228 tcp_timeout = mkOption {
229 type = types.int;
230 default = 30;
231 description = "Seconds before a TCP connection times out.";
232 };
233
234 ttl_floor = mkOption {
235 type = types.int;
236 default = 60;
237 description = "Minimum TTL enforced on all DNS responses.";
238 };
239
240 slow_query_threshold_ms = mkOption {
241 type = types.int;
242 default = 50;
243 description = "Log a warning for queries slower than this (milliseconds).";
244 };
245
246 ns = mkOption {
247 type = types.listOf types.str;
248 default = [ "ns1.example.com." "ns2.example.com." ];
249 description = "NS records to serve for all zones (fully qualified, trailing dot).";
250 };
251
252 metrics_bind = mkOption {
253 type = types.str;
254 default = "0.0.0.0:9100";
255 description = "Address and port for the DNS metrics HTTP server.";
256 };
257
258 soa = {
259 ttl = mkOption {
260 type = types.int;
261 default = 3600;
262 description = "SOA record TTL in seconds.";
263 };
264
265 refresh = mkOption {
266 type = types.int;
267 default = 3600;
268 description = "SOA refresh interval in seconds.";
269 };
270
271 retry = mkOption {
272 type = types.int;
273 default = 900;
274 description = "SOA retry interval in seconds.";
275 };
276
277 expire = mkOption {
278 type = types.int;
279 default = 604800;
280 description = "SOA expire interval in seconds.";
281 };
282
283 minimum = mkOption {
284 type = types.int;
285 default = 300;
286 description = "SOA minimum (negative cache) TTL in seconds.";
287 };
288
289 mname = mkOption {
290 type = types.str;
291 default = "ns1.example.com.";
292 description = "SOA MNAME — primary nameserver, fully qualified.";
293 };
294
295 rname = mkOption {
296 type = types.str;
297 default = "admin.example.com.";
298 description = "SOA RNAME — admin email in DNS format, fully qualified.";
299 };
300 };
301 };
302
303 # -----------------------------------------------------------------
304 # Verify
305 # -----------------------------------------------------------------
306 verify = {
307 enable = mkEnableOption "onis verify service";
308
309 package = mkOption {
310 type = types.package;
311 default = self.packages.${pkgs.system}.onis-verify;
312 defaultText = lib.literalExpression "self.packages.\${pkgs.system}.onis-verify";
313 description = "The onis-verify package to use.";
314 };
315
316 appview_url = mkOption {
317 type = types.str;
318 default = "http://localhost:3000";
319 description = "URL of the onis appview API.";
320 };
321
322 bind = mkOption {
323 type = types.str;
324 default = "0.0.0.0";
325 description = "Address for the verify HTTP server to listen on.";
326 };
327
328 port = mkOption {
329 type = types.port;
330 default = 3001;
331 description = "Port for the verify HTTP server.";
332 };
333
334 check_interval = mkOption {
335 type = types.int;
336 default = 60;
337 description = "Seconds between scheduled verification runs.";
338 };
339
340 recheck_interval = mkOption {
341 type = types.int;
342 default = 3600;
343 description = "Seconds a zone must be stale before rechecking.";
344 };
345
346 expected_ns = mkOption {
347 type = types.listOf types.str;
348 default = [ "ns1.example.com" "ns2.example.com" ];
349 description = "Expected NS records that indicate correct delegation.";
350 };
351
352 nameservers = mkOption {
353 type = types.listOf types.str;
354 default = [ ];
355 description = "Optional custom resolver IP addresses.";
356 };
357
358 dns_port = mkOption {
359 type = types.port;
360 default = 53;
361 description = "Port used when resolving against custom nameservers.";
362 };
363 };
364 };
365
366 config = mkMerge [
367 (mkIf cfg.appview.enable {
368 systemd.services.onis-appview = {
369 description = "onis appview — ATProto DNS appview";
370 wantedBy = [ "multi-user.target" ];
371 after = [ "network.target" ];
372 environment.ONIS_CONFIG = "${configFile}";
373 serviceConfig = {
374 ExecStart = "${cfg.appview.package}/bin/onis-appview";
375 DynamicUser = true;
376 StateDirectory = "onis-appview";
377 Restart = "on-failure";
378 RestartSec = 5;
379 };
380 };
381 })
382
383 (mkIf cfg.dns.enable {
384 systemd.services.onis-dns = {
385 description = "onis DNS — authoritative DNS server";
386 wantedBy = [ "multi-user.target" ];
387 after = [ "network.target" ];
388 environment.ONIS_CONFIG = "${configFile}";
389 serviceConfig = {
390 ExecStart = "${cfg.dns.package}/bin/onis-dns";
391 DynamicUser = true;
392 StateDirectory = "onis-dns";
393 Restart = "on-failure";
394 RestartSec = 5;
395 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
396 };
397 };
398 })
399
400 (mkIf cfg.verify.enable {
401 systemd.services.onis-verify = {
402 description = "onis verify — DNS delegation checker";
403 wantedBy = [ "multi-user.target" ];
404 after = [ "network.target" ];
405 environment.ONIS_CONFIG = "${configFile}";
406 serviceConfig = {
407 ExecStart = "${cfg.verify.package}/bin/onis-verify";
408 DynamicUser = true;
409 StateDirectory = "onis-verify";
410 Restart = "on-failure";
411 RestartSec = 5;
412 };
413 };
414 })
415 ];
416 };
417
418 };
419}