diff --git a/flake.nix b/flake.nix index 6dd584b..5cb32f0 100644 --- a/flake.nix +++ b/flake.nix @@ -14,14 +14,15 @@ }; }; - outputs = inputs @ { - self, - nixpkgs, - flake-parts, - rust-overlay, - ... - }: - flake-parts.lib.mkFlake {inherit inputs;} { + outputs = + inputs@{ + self, + nixpkgs, + flake-parts, + rust-overlay, + ... + }: + flake-parts.lib.mkFlake { inherit inputs; } { systems = [ "x86_64-linux" "aarch64-linux" @@ -29,197 +30,278 @@ "aarch64-darwin" ]; - perSystem = { - self', - pkgs, - system, - ... - }: let - rustPkgs = import nixpkgs { - inherit system; - overlays = [(import rust-overlay)]; - }; - rustToolchain = rustPkgs.rust-bin.stable.latest.default.override { - extensions = ["rust-src" "rust-analyzer" "clippy" "rustfmt"]; - }; - commonBuildInputs = with pkgs; [openssl sqlite]; - commonNativeBuildInputs = with pkgs; [cmake perl pkg-config]; - in { - packages = { - default = pkgs.rustPlatform.buildRustPackage { - pname = "iota"; - version = "0.1.0"; - src = ./.; - cargoBuildFlags = [ - "-p" - "iota" - "-p" - "iota-daemon" - "-p" - "iota-updater" - "-p" - "iota-installer" + perSystem = + { + self', + pkgs, + system, + ... + }: + let + rustPkgs = import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) ]; + }; + rustToolchain = rustPkgs.rust-bin.stable.latest.default.override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + "rustfmt" ]; - cargoLock = { - lockFile = ./Cargo.lock; - allowBuiltinFetchGit = true; + }; + commonBuildInputs = with pkgs; [ + openssl + sqlite + ]; + commonNativeBuildInputs = with pkgs; [ + cmake + perl + pkg-config + ]; + in + { + packages = { + default = pkgs.rustPlatform.buildRustPackage { + pname = "iota"; + version = "0.1.0"; + src = ./.; + cargoBuildFlags = [ + "-p" + "iota" + "-p" + "iota-daemon" + "-p" + "iota-updater" + "-p" + "iota-installer" + ]; + cargoLock = { + lockFile = ./Cargo.lock; + allowBuiltinFetchGit = true; + }; + nativeBuildInputs = commonNativeBuildInputs; + buildInputs = commonBuildInputs; + dontUseCmakeConfigure = true; + MTP_TYPE_MAPS = "${inputs.mtp-type-maps}/type-maps.yaml"; + passthru.dataDir = "/var/lib/iota"; }; - nativeBuildInputs = commonNativeBuildInputs; - buildInputs = commonBuildInputs; - dontUseCmakeConfigure = true; - MTP_TYPE_MAPS = "${inputs.mtp-type-maps}/type-maps.yaml"; - passthru.dataDir = "/var/lib/iota"; + + iota-daemon = self'.packages.default.overrideAttrs (old: { + pname = "iota-daemon"; + cargoBuildFlags = [ + "-p" + "iota-daemon" + ]; + postInstall = '' + for f in $out/bin/*; do + if [ "$(basename "$f")" != "iota-daemon" ]; then + rm "$f" + fi + done + ''; + }); + + iota-ui = self'.packages.default.overrideAttrs (old: { + pname = "iota-ui"; + cargoBuildFlags = [ + "-p" + "iota" + ]; + postInstall = '' + for f in $out/bin/*; do + if [ "$(basename "$f")" != "iota" ]; then + rm "$f" + fi + done + if [ -f "$out/bin/iota" ]; then + mv "$out/bin/iota" "$out/bin/iota-ui" + fi + ''; + }); }; - iota-daemon = self'.packages.default.overrideAttrs (old: { - pname = "iota-daemon"; - cargoBuildFlags = ["-p" "iota-daemon"]; - postInstall = '' - for f in $out/bin/*; do - if [ "$(basename "$f")" != "iota-daemon" ]; then - rm "$f" - fi - done - ''; - }); - - iota-ui = self'.packages.default.overrideAttrs (old: { - pname = "iota-ui"; - cargoBuildFlags = ["-p" "iota"]; - postInstall = '' - for f in $out/bin/*; do - if [ "$(basename "$f")" != "iota" ]; then - rm "$f" - fi - done - if [ -f "$out/bin/iota" ]; then - mv "$out/bin/iota" "$out/bin/iota-ui" - fi - ''; - }); + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + rustToolchain + git + cmake + perl + pkg-config + ]; + buildInputs = commonBuildInputs; + }; }; - devShells.default = pkgs.mkShell { - nativeBuildInputs = with pkgs; [rustToolchain git cmake perl pkg-config]; - buildInputs = commonBuildInputs; - }; - }; - flake = { - nixosModules.default = { - config, - pkgs, - lib, - ... - }: let - cfg = config.services.iota; - defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default or (throw "iota: no pre-built package for system ${pkgs.stdenv.hostPlatform.system}"); + nixosModules.default = + { + config, + pkgs, + lib, + ... + }: + let + cfg = config.services.iota; + defaultPackage = + self.packages.${pkgs.stdenv.hostPlatform.system}.default + or (throw "iota: no pre-built package for system ${pkgs.stdenv.hostPlatform.system}"); - configFormat = pkgs.formats.yaml {}; - configFile = - if cfg.settingsFile != null - then cfg.settingsFile - else configFormat.generate "iota-config.yaml" cfg.settings; + configFormat = pkgs.formats.yaml { }; + effectiveSettings = lib.recursiveUpdate { + port = cfg.port; + web = { + mode = "network"; + bind = cfg.bindAddress; + port = cfg.port; + required = true; + } + // lib.optionalAttrs (cfg.certFile != null) { + certificate = "${cfg.stateDir}/tls/cert.pem"; + key = "${cfg.stateDir}/tls/key.pem"; + }; + } cfg.settings; + configFile = + if cfg.settingsFile != null then + cfg.settingsFile + else + configFormat.generate "iota-config.yaml" effectiveSettings; - descriptionText = "Tensamin Iota daemon"; - in { - options.services.iota = { - enable = lib.mkEnableOption "Enable the Iota service."; + descriptionText = "Tensamin Iota daemon"; + in + { + options.services.iota = { + enable = lib.mkEnableOption "Enable the Iota service."; - stateDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/iota"; - description = "Persistent mutable Iota state."; - }; - cacheDir = lib.mkOption { type = lib.types.str; default = "/var/cache/iota"; }; - runtimeDir = lib.mkOption { type = lib.types.str; default = "/run/iota"; }; - logDir = lib.mkOption { type = lib.types.str; default = "/var/log/iota"; }; - assetDir = lib.mkOption { type = lib.types.str; default = "${cfg.package}/share/iota/web"; }; + stateDir = lib.mkOption { + type = lib.types.str; + default = "/var/lib/iota"; + description = "Persistent mutable Iota state."; + }; + cacheDir = lib.mkOption { + type = lib.types.str; + default = "/var/cache/iota"; + }; + runtimeDir = lib.mkOption { + type = lib.types.str; + default = "/run/iota"; + }; + logDir = lib.mkOption { + type = lib.types.str; + default = "/var/log/iota"; + }; + assetDir = lib.mkOption { + type = lib.types.str; + default = "${cfg.package}/share/iota/web"; + }; - certFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - description = "Path to the SSL certificate file (cert.pem)."; - }; + certFile = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Path to the SSL certificate file (cert.pem)."; + }; - keyFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - description = "Path to the SSL private key file (cert.key)."; - }; + keyFile = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Path to the SSL private key file (cert.key)."; + }; - environmentFiles = lib.mkOption { - type = lib.types.listOf lib.types.path; - default = []; - description = "Environment files to load for the Iota service."; - }; + environmentFiles = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Environment files to load for the Iota service."; + }; - openFirewall = lib.mkOption { - type = lib.types.bool; - default = true; - description = "Whether to open the firewall for ports used by Iota."; - }; + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to open the firewall for ports used by Iota."; + }; - bindAddress = lib.mkOption { - type = lib.types.str; - default = "0.0.0.0"; - description = "IP address to bind the HTTP server to."; - }; + bindAddress = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "IP address to bind the HTTP server to."; + }; - package = lib.mkOption { - type = lib.types.package; - default = defaultPackage; - description = "The Iota package to use."; - }; + port = lib.mkOption { + type = lib.types.port; + default = 1984; + description = "TCP and UDP port for protocol-only Iota."; + }; - settings = lib.mkOption { - type = lib.types.attrs; - default = {}; - description = "Configuration attributes for Iota, written to YAML."; - }; + omegaApiUrl = lib.mkOption { + type = lib.types.str; + default = "https://omega.tensamin.net"; + description = "Omega discovery API URL."; + }; - settingsFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - description = "Path to an existing YAML file to use instead of generating from settings."; - }; - }; + package = lib.mkOption { + type = lib.types.package; + default = defaultPackage; + description = "The Iota package to use."; + }; - config = lib.mkIf cfg.enable { - users.users.iota = { - isSystemUser = true; - group = "iota"; - home = cfg.stateDir; - createHome = true; - description = "Iota service user"; - shell = pkgs.bash; - }; + settings = lib.mkOption { + type = lib.types.attrs; + default = { }; + description = "Configuration attributes for Iota, written to YAML."; + }; - users.groups.iota = {}; - - systemd.sockets.iota = { - description = "${descriptionText} IPC socket"; - wantedBy = ["sockets.target"]; - socketConfig = { - ListenStream = "/run/iota/iota.sock"; - SocketMode = "0660"; - SocketUser = "iota"; - SocketGroup = "iota"; - DirectoryMode = "0750"; - Backlog = 5; - RemoveOnStop = "true"; - NonBlocking = true; + settingsFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to an existing YAML file to use instead of generating from settings."; }; }; - systemd.services.iota = { - description = descriptionText; - wantedBy = ["multi-user.target"]; - after = ["network.target" "iota.socket"]; - requires = ["iota.socket"]; - - serviceConfig = + config = lib.mkIf cfg.enable { + assertions = [ { + assertion = (cfg.certFile == null) == (cfg.keyFile == null); + message = "services.iota: certFile and keyFile must be set together."; + } + ]; + + users.users.iota = { + isSystemUser = true; + group = "iota"; + home = cfg.stateDir; + createHome = true; + description = "Iota service user"; + shell = pkgs.bash; + }; + + users.groups.iota = { }; + + systemd.sockets.iota = { + description = "${descriptionText} IPC socket"; + wantedBy = [ "sockets.target" ]; + socketConfig = { + ListenStream = "/run/iota/iota.sock"; + SocketMode = "0660"; + SocketUser = "iota"; + SocketGroup = "iota"; + DirectoryMode = "0750"; + Backlog = 5; + RemoveOnStop = "true"; + NonBlocking = true; + }; + }; + + systemd.services.iota = { + description = descriptionText; + wantedBy = [ "multi-user.target" ]; + after = [ + "network.target" + "iota.socket" + ]; + requires = [ "iota.socket" ]; + + environment.OMEGA_API_URL = cfg.omegaApiUrl; + + serviceConfig = { Type = "simple"; User = "iota"; Group = "iota"; @@ -244,15 +326,23 @@ KillMode = "mixed"; KillSignal = "SIGTERM"; - AmbientCapabilities = ["CAP_NET_BIND_SERVICE"]; - CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"]; + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; NoNewPrivileges = true; - ReadWritePaths = [cfg.stateDir cfg.cacheDir cfg.runtimeDir cfg.logDir]; - ReadOnlyPaths = [configFile cfg.assetDir]; + ReadWritePaths = [ + cfg.stateDir + cfg.cacheDir + cfg.runtimeDir + cfg.logDir + ]; + ReadOnlyPaths = [ + configFile + cfg.assetDir + ]; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; @@ -261,7 +351,6 @@ LockPersonality = true; MemoryDenyWriteExecute = true; Environment = [ - "BIND_ADDRESS=${cfg.bindAddress}" "IOTA_SOCKET=/run/iota/iota.sock" "IOTA_CONFIG_FILE=${configFile}" "IOTA_STATE_DIR=${cfg.stateDir}" @@ -273,17 +362,24 @@ "IOTA_SUPERVISOR=systemd" ]; } - // lib.optionalAttrs (cfg.environmentFiles != []) { + // lib.optionalAttrs (cfg.certFile != null) { + ExecStartPre = "+${pkgs.writeShellScript "iota-setup-tls" '' + install -d -m 0700 -o iota -g iota ${cfg.stateDir}/tls + install -m 0644 -o iota -g iota ${cfg.certFile} ${cfg.stateDir}/tls/cert.pem + install -m 0600 -o iota -g iota ${cfg.keyFile} ${cfg.stateDir}/tls/key.pem + ''}"; + } + // lib.optionalAttrs (cfg.environmentFiles != [ ]) { EnvironmentFile = cfg.environmentFiles; }; - }; + }; - networking.firewall = lib.mkIf cfg.openFirewall { - allowedTCPPorts = [1984]; - allowedUDPPorts = [1984]; + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + allowedUDPPorts = [ cfg.port ]; + }; }; }; - }; }; }; }