diff --git a/Cargo.lock b/Cargo.lock index 7b19efe..420eb6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4317,7 +4317,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ttp-core" version = "0.1.0" -source = "git+https://git.methanium.net/Tensamin/TTP.git#929cb9d3a6aebebe6365973f13062ac2a8e03af6" +source = "git+https://git.methanium.net/Tensamin/TTP.git#7e5d1953df8592a1feba0f390d205d0ef61a3119" dependencies = [ "base64", "byteorder", @@ -4330,7 +4330,7 @@ dependencies = [ [[package]] name = "ttp-native" version = "0.1.0" -source = "git+https://git.methanium.net/Tensamin/TTP.git#929cb9d3a6aebebe6365973f13062ac2a8e03af6" +source = "git+https://git.methanium.net/Tensamin/TTP.git#7e5d1953df8592a1feba0f390d205d0ef61a3119" dependencies = [ "quinn", "rustls", diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..fb5bf79 --- /dev/null +++ b/flake.lock @@ -0,0 +1,99 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1777168982, + "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", + "ttp": "ttp" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1778987862, + "narHash": "sha256-V3qGt9P1eJP/r/1ONablphfGiH0RP4agQhrRANpDYx8=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "6f44d8874ac29806c8d5cae42bf8e19ebb5ce0d3", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "ttp": { + "flake": false, + "locked": { + "lastModified": 1778948017, + "narHash": "sha256-hqBYSZnPq7f/F2Z6nJK+6a8ITk6WRCcVBcNT0CR6SnM=", + "rev": "7e5d1953df8592a1feba0f390d205d0ef61a3119", + "revCount": 117, + "type": "git", + "url": "https://git.methanium.net/Tensamin/TTP.git" + }, + "original": { + "rev": "7e5d1953df8592a1feba0f390d205d0ef61a3119", + "type": "git", + "url": "https://git.methanium.net/Tensamin/TTP.git" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b422ab5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,247 @@ +{ + description = "Iota"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + ttp = { + url = "git+https://git.methanium.net/Tensamin/TTP.git?rev=7e5d1953df8592a1feba0f390d205d0ef61a3119"; + flake = false; + }; + }; + + outputs = inputs@{ self, nixpkgs, flake-parts, rust-overlay, ttp, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "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" ]; + }; + in + { + packages = { + default = self'.packages.iota; + iota = pkgs.rustPlatform.buildRustPackage { + pname = "iota"; + version = "0.1.0"; + src = ./.; + cargoLock = { + lockFile = ./Cargo.lock; + allowBuiltinFetchGit = true; + }; + nativeBuildInputs = with pkgs; [ cmake perl pkg-config ]; + buildInputs = with pkgs; [ openssl sqlite ]; + dontUseCmakeConfigure = true; + preConfigure = '' + if [ -d ../cargo-vendor-dir/ttp-core-0.1.0 ]; then + cp ${ttp}/ttp-codec.json ../cargo-vendor-dir/ttp-codec.json + fi + ''; + postInstall = '' + mv $out/bin/iota-core $out/bin/iota + for f in $out/bin/*; do + if [ "$(basename "$f")" != "iota" ]; then + rm "$f" + fi + done + ''; + passthru.dataDir = "/var/lib/iota"; + }; + }; + + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ rustToolchain git cmake perl pkg-config ]; + buildInputs = with pkgs; [ openssl sqlite ]; + }; + }; + + 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}"); + + configFile = if cfg.settingsFile != null then cfg.settingsFile else + pkgs.writeText "iota-config.json" (builtins.toJSON cfg.settings); + + descriptionText = "Iota Service" + + lib.optionalString cfg.useTmux " (attach TUI: tmux -S ${cfg.dataDir}/tmux.sock attach -t iota)"; + in + { + options.services.iota = { + enable = lib.mkEnableOption "the Iota service"; + + dataDir = lib.mkOption { + type = lib.types.str; + default = cfg.package.passthru.dataDir or "/var/lib/iota"; + defaultText = lib.literalExpression ''config.services.iota.package.passthru.dataDir or "/var/lib/iota"''; + description = "Directory where Iota stores its data, config, and certificates."; + }; + + certFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + 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)."; + }; + + environmentFiles = lib.mkOption { + type = lib.types.listOf lib.types.path; + 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."; + }; + + ttpBind = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "IP address to bind the TTP/QUIC 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."; + }; + + useTmux = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to run Iota inside a tmux session for shared TUI access."; + }; + + settings = lib.mkOption { + type = lib.types.attrs; + default = { }; + description = "Configuration attributes for Iota, written to config.json."; + }; + + settingsFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to an existing config.json file to use instead of generating from settings."; + }; + }; + + config = lib.mkIf cfg.enable { + users.users.iota = { + isSystemUser = true; + group = "iota"; + home = cfg.dataDir; + createHome = true; + description = "Iota service user"; + }; + + users.groups.iota = { }; + + systemd.services.iota = { + description = descriptionText; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + path = [ pkgs.tmux pkgs.bash pkgs.coreutils pkgs.systemd ]; + + serviceConfig = { + Type = if cfg.useTmux then "forking" else "simple"; + User = "iota"; + Group = "iota"; + WorkingDirectory = cfg.dataDir; + + ExecStart = if cfg.useTmux then + pkgs.writeShellScript "iota-start" '' + ${pkgs.tmux}/bin/tmux -S ${cfg.dataDir}/tmux.sock new-session -d -s iota \ + "${pkgs.bash}/bin/bash -lc 'exec > >(${pkgs.coreutils}/bin/tee -a ${cfg.dataDir}/iota-output.log >(${pkgs.systemd}/bin/systemd-cat -t iota-daemon)) 2>&1; ${cfg.package}/bin/iota; status=$?; printf \"\nProcess exited with status %s. Press any key to close this tmux session...\" \"\$status\"; read -r -n 1; exit \"\$status\"'" + '' + else + "${cfg.package}/bin/iota"; + + ExecStop = if cfg.useTmux then + (pkgs.writeShellScript "iota-stop" '' + ${pkgs.tmux}/bin/tmux -S ${cfg.dataDir}/tmux.sock kill-session -t iota 2>/dev/null || true + '') + else + null; + + ExecStartPre = [ + ("+" + pkgs.writeShellScript "iota-setup" '' + mkdir -p ${cfg.dataDir}/certs + + ${lib.optionalString (cfg.certFile != null) "ln -sf ${cfg.certFile} ${cfg.dataDir}/certs/cert.pem"} + ${lib.optionalString (cfg.keyFile != null) "ln -sf ${cfg.keyFile} ${cfg.dataDir}/certs/cert.key"} + + install -m 644 ${configFile} ${cfg.dataDir}/config.json + + chown -R iota:iota ${cfg.dataDir} + + ${lib.optionalString cfg.useTmux '' + echo "Iota started under tmux. Attach with: tmux -S ${cfg.dataDir}/tmux.sock attach -t iota" >&2 + ''} + '') + ]; + + Restart = "always"; + RestartSec = "5s"; + + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + NoNewPrivileges = true; + ReadWritePaths = [ cfg.dataDir ]; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + Environment = [ + "TTP_BIND=${cfg.ttpBind}" + "BIND_ADDRESS=${cfg.bindAddress}" + ]; + } // lib.optionalAttrs (cfg.environmentFiles != [ ]) { + EnvironmentFile = cfg.environmentFiles; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ 1984 ]; + allowedUDPPorts = [ 1984 ]; + }; + }; + }; + }; + }; +} diff --git a/web-ui/src/server.rs b/web-ui/src/server.rs index edecfdd..263256e 100644 --- a/web-ui/src/server.rs +++ b/web-ui/src/server.rs @@ -19,10 +19,13 @@ use tokio::sync::oneshot; pub async fn start(port: u16) -> bool { let (tx, rx) = oneshot::channel::(); + let bind_addr = std::env::var("BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0".to_string()); + let bind_ip: std::net::IpAddr = bind_addr.parse().expect("Invalid BIND_ADDRESS"); + let _ = tokio::spawn(async move { let server = match load_tls_config() { Ok(Some(tls_config)) => { - log!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port); + log!("HTTPS (HTTP/2) Server running on {}:{}", bind_addr, port); let _config = (*tls_config).clone(); HttpServer::new(move || { App::new() @@ -30,19 +33,19 @@ pub async fn start(port: u16) -> bool { .configure(api_config) .default_service(web::to(web_path_parser::handle)) }) - .bind(("0.0.0.0", port)) + .bind((bind_ip, port)) .unwrap() .run() } Ok(_) => { - log!("HTTP Server running on 0.0.0.0:{}", port); + log!("HTTP Server running on {}:{}", bind_addr, port); HttpServer::new(move || { App::new() .app_data(web::Data::new(false)) .configure(api_config) .default_service(web::to(web_path_parser::handle)) }) - .bind(("0.0.0.0", port)) + .bind((bind_ip, port)) .unwrap() .run() }