{ description = "onis — decentralized DNS over ATProto"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; crane.url = "github:ipetkov/crane"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, crane, flake-utils, ... }: let perSystem = flake-utils.lib.eachSystem [ "x86_64-linux" ] (system: let pkgs = import nixpkgs { inherit system; }; craneLib = crane.mkLib pkgs; commonArgs = { src = let sqlFilter = path: _type: builtins.match ".*\.sql$" path != null; sqlOrCargo = path: type: (sqlFilter path type) || (craneLib.filterCargoSources path type); in pkgs.lib.cleanSourceWith { src = ./.; filter = sqlOrCargo; }; buildInputs = with pkgs; [ openssl ]; nativeBuildInputs = with pkgs; [ pkg-config ]; }; cargoArtifacts = craneLib.buildDepsOnly commonArgs; onis-appview = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; cargoExtraArgs = "--bin onis-appview"; }); onis-dns = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; cargoExtraArgs = "--bin onis-dns"; }); onis-verify = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; cargoExtraArgs = "--bin onis-verify"; }); in { packages = { inherit onis-appview onis-dns onis-verify; default = pkgs.symlinkJoin { name = "onis"; paths = [ onis-appview onis-dns onis-verify ]; }; }; checks = { onis-clippy = craneLib.cargoClippy (commonArgs // { inherit cargoArtifacts; cargoClippyExtraArgs = "-- --deny warnings"; }); onis-fmt = craneLib.cargoFmt { src = commonArgs.src; }; onis-test = craneLib.cargoNextest (commonArgs // { inherit cargoArtifacts; }); }; devShells.default = craneLib.devShell { checks = self.checks.${system}; packages = with pkgs; [ clippy rust-analyzer sqlx-cli cargo-watch pkg-config openssl sqlite dig ]; }; } ); in perSystem // { nixosModules.default = { config, lib, pkgs, ... }: let inherit (lib) mkEnableOption mkOption types mkIf mkMerge; cfg = config.services.onis; settingsFormat = pkgs.formats.toml { }; configFile = settingsFormat.generate "onis.toml" { appview = { inherit (cfg.appview) bind tap_url tap_acks tap_reconnect_delay index_path db_dir; database = { inherit (cfg.appview.database) busy_timeout user_max_connections index_max_connections; }; }; dns = { inherit (cfg.dns) appview_url bind port tcp_timeout ttl_floor slow_query_threshold_ms ns metrics_bind; soa = { inherit (cfg.dns.soa) ttl refresh retry expire minimum mname rname; }; }; verify = { inherit (cfg.verify) appview_url bind port check_interval recheck_interval expected_ns nameservers dns_port; }; }; in { options.services.onis = { # ----------------------------------------------------------------- # Appview # ----------------------------------------------------------------- appview = { enable = mkEnableOption "onis appview service"; package = mkOption { type = types.package; default = self.packages.${pkgs.system}.onis-appview; defaultText = lib.literalExpression "self.packages.\${pkgs.system}.onis-appview"; description = "The onis-appview package to use."; }; bind = mkOption { type = types.str; default = "localhost:3000"; description = "Address and port for the appview HTTP server."; }; tap_url = mkOption { type = types.str; default = "ws://localhost:2480/channel"; description = "WebSocket URL for the TAP firehose."; }; tap_acks = mkOption { type = types.bool; default = true; description = "Whether to acknowledge TAP messages."; }; tap_reconnect_delay = mkOption { type = types.int; default = 5; description = "Seconds to wait before reconnecting after a TAP connection error."; }; index_path = mkOption { type = types.str; default = "/var/lib/onis-appview/index.db"; description = "Path to the shared index SQLite database."; }; db_dir = mkOption { type = types.str; default = "/var/lib/onis-appview/dbs"; description = "Directory for per-user SQLite databases."; }; database = { busy_timeout = mkOption { type = types.int; default = 5; description = "Seconds to wait when the database is locked."; }; user_max_connections = mkOption { type = types.int; default = 5; description = "Max connections for per-user database pools."; }; index_max_connections = mkOption { type = types.int; default = 10; description = "Max connections for the shared index database pool."; }; }; }; # ----------------------------------------------------------------- # DNS # ----------------------------------------------------------------- dns = { enable = mkEnableOption "onis DNS server"; package = mkOption { type = types.package; default = self.packages.${pkgs.system}.onis-dns; defaultText = lib.literalExpression "self.packages.\${pkgs.system}.onis-dns"; description = "The onis-dns package to use."; }; appview_url = mkOption { type = types.str; default = "http://localhost:3000"; description = "URL of the onis appview API."; }; bind = mkOption { type = types.str; default = "0.0.0.0"; description = "Address for the DNS server to listen on."; }; port = mkOption { type = types.port; default = 53; description = "Port for the DNS server."; }; tcp_timeout = mkOption { type = types.int; default = 30; description = "Seconds before a TCP connection times out."; }; ttl_floor = mkOption { type = types.int; default = 60; description = "Minimum TTL enforced on all DNS responses."; }; slow_query_threshold_ms = mkOption { type = types.int; default = 50; description = "Log a warning for queries slower than this (milliseconds)."; }; ns = mkOption { type = types.listOf types.str; default = [ "ns1.example.com." "ns2.example.com." ]; description = "NS records to serve for all zones (fully qualified, trailing dot)."; }; metrics_bind = mkOption { type = types.str; default = "0.0.0.0:9100"; description = "Address and port for the DNS metrics HTTP server."; }; soa = { ttl = mkOption { type = types.int; default = 3600; description = "SOA record TTL in seconds."; }; refresh = mkOption { type = types.int; default = 3600; description = "SOA refresh interval in seconds."; }; retry = mkOption { type = types.int; default = 900; description = "SOA retry interval in seconds."; }; expire = mkOption { type = types.int; default = 604800; description = "SOA expire interval in seconds."; }; minimum = mkOption { type = types.int; default = 300; description = "SOA minimum (negative cache) TTL in seconds."; }; mname = mkOption { type = types.str; default = "ns1.example.com."; description = "SOA MNAME — primary nameserver, fully qualified."; }; rname = mkOption { type = types.str; default = "admin.example.com."; description = "SOA RNAME — admin email in DNS format, fully qualified."; }; }; }; # ----------------------------------------------------------------- # Verify # ----------------------------------------------------------------- verify = { enable = mkEnableOption "onis verify service"; package = mkOption { type = types.package; default = self.packages.${pkgs.system}.onis-verify; defaultText = lib.literalExpression "self.packages.\${pkgs.system}.onis-verify"; description = "The onis-verify package to use."; }; appview_url = mkOption { type = types.str; default = "http://localhost:3000"; description = "URL of the onis appview API."; }; bind = mkOption { type = types.str; default = "0.0.0.0"; description = "Address for the verify HTTP server to listen on."; }; port = mkOption { type = types.port; default = 3001; description = "Port for the verify HTTP server."; }; check_interval = mkOption { type = types.int; default = 60; description = "Seconds between scheduled verification runs."; }; recheck_interval = mkOption { type = types.int; default = 3600; description = "Seconds a zone must be stale before rechecking."; }; expected_ns = mkOption { type = types.listOf types.str; default = [ "ns1.example.com" "ns2.example.com" ]; description = "Expected NS records that indicate correct delegation."; }; nameservers = mkOption { type = types.listOf types.str; default = [ ]; description = "Optional custom resolver IP addresses."; }; dns_port = mkOption { type = types.port; default = 53; description = "Port used when resolving against custom nameservers."; }; }; }; config = mkMerge [ (mkIf cfg.appview.enable { systemd.services.onis-appview = { description = "onis appview — ATProto DNS appview"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; environment.ONIS_CONFIG = "${configFile}"; serviceConfig = { ExecStart = "${cfg.appview.package}/bin/onis-appview"; DynamicUser = true; StateDirectory = "onis-appview"; Restart = "on-failure"; RestartSec = 5; }; }; }) (mkIf cfg.dns.enable { systemd.services.onis-dns = { description = "onis DNS — authoritative DNS server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; environment.ONIS_CONFIG = "${configFile}"; serviceConfig = { ExecStart = "${cfg.dns.package}/bin/onis-dns"; DynamicUser = true; StateDirectory = "onis-dns"; Restart = "on-failure"; RestartSec = 5; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; }; }; }) (mkIf cfg.verify.enable { systemd.services.onis-verify = { description = "onis verify — DNS delegation checker"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; environment.ONIS_CONFIG = "${configFile}"; serviceConfig = { ExecStart = "${cfg.verify.package}/bin/onis-verify"; DynamicUser = true; StateDirectory = "onis-verify"; Restart = "on-failure"; RestartSec = 5; }; }; }) ]; }; }; }