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..97b1bcc --- /dev/null +++ b/flake.nix @@ -0,0 +1,221 @@ +{ + description = "Omega"; + + 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.omega; + omega = pkgs.rustPlatform.buildRustPackage { + pname = "omega"; + version = "0.1.0"; + src = ./.; + cargoLock = { + lockFile = ./Cargo.lock; + allowBuiltinFetchGit = true; + }; + nativeBuildInputs = with pkgs; [ cmake perl pkg-config ]; + buildInputs = with pkgs; [ openssl libmysqlclient ]; + 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 + ''; + }; + }; + + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ rustToolchain git cmake perl pkg-config ]; + buildInputs = with pkgs; [ openssl libmysqlclient ]; + }; + }; + + flake = { + nixosModules.default = { config, pkgs, lib, ... }: + let + cfg = config.services.omega; + defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default or (throw "omega: no pre-built package for system ${pkgs.stdenv.hostPlatform.system}"); + in + { + options.services.omega = { + enable = lib.mkEnableOption "Omega, Tensamin's central server"; + + dataDir = lib.mkOption { + type = lib.types.str; + default = "/var/lib/omega"; + description = "Directory where Omega stores its data and reads certificates from."; + }; + + apiCertFile = lib.mkOption { + type = lib.types.path; + description = "Path to the SSL certificate file for the web/API server (e.g. ACME fullchain.pem). Copied to server_cert.pem at runtime."; + }; + + apiKeyFile = lib.mkOption { + type = lib.types.path; + description = "Path to the SSL private key file for the web/API server (e.g. ACME key.pem). Converted to PKCS#8 and copied to server_key.pem at runtime."; + }; + + transportCertFile = lib.mkOption { + type = lib.types.path; + description = "Path to the SSL certificate file for the TTP/QUIC transport (e.g. ACME fullchain.pem). Copied to transport_cert.pem at runtime."; + }; + + transportKeyFile = lib.mkOption { + type = lib.types.path; + description = "Path to the SSL private key file for the TTP/QUIC transport (e.g. ACME key.pem). Converted to PKCS#8 and copied to transport_key.pem at runtime."; + }; + + environmentFiles = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = [ ]; + description = '' + Environment files to load for the Omega service. + Must provide at least DB_URL, PRIVATE_KEY, and PUBLIC_KEY. + ''; + }; + + apiPort = lib.mkOption { + type = lib.types.port; + default = 9188; + description = "Port for the web/API server."; + }; + + transportPort = lib.mkOption { + type = lib.types.port; + default = 9187; + description = "Port for the TTP/QUIC transport server."; + }; + + ttpBind = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "IP address to bind the TTP/QUIC transport server to."; + }; + + bindAddress = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "IP address to bind the HTTP/API server to."; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to open the firewall for ports used by Omega."; + }; + + package = lib.mkOption { + type = lib.types.package; + default = defaultPackage; + description = "The Omega package to use."; + }; + }; + + config = lib.mkIf cfg.enable { + users.users.omega = { + isSystemUser = true; + group = "omega"; + home = cfg.dataDir; + createHome = true; + description = "Omega service user"; + }; + + users.groups.omega = { }; + + systemd.services.omega = { + description = "Tensamin Omega"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + Type = "simple"; + User = "omega"; + Group = "omega"; + WorkingDirectory = cfg.dataDir; + ExecStart = "${cfg.package}/bin/omega"; + Restart = "always"; + RestartSec = "5s"; + + Environment = [ + "API_PORT=${toString cfg.apiPort}" + "OMIKRON_PORT=${toString cfg.transportPort}" + "TTP_BIND=${cfg.ttpBind}" + "BIND_ADDRESS=${cfg.bindAddress}" + ]; + + EnvironmentFile = cfg.environmentFiles; + + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + + ExecStartPre = [ + ("+" + pkgs.writeShellScript "omega-setup-certs" '' + mkdir -p ${cfg.dataDir}/certs + cp ${cfg.apiCertFile} ${cfg.dataDir}/certs/server_cert.pem + ${pkgs.openssl}/bin/openssl pkcs8 -topk8 -nocrypt \ + -in ${cfg.apiKeyFile} \ + -out ${cfg.dataDir}/certs/server_key.pem + cp ${cfg.transportCertFile} ${cfg.dataDir}/certs/transport_cert.pem + ${pkgs.openssl}/bin/openssl pkcs8 -topk8 -nocrypt \ + -in ${cfg.transportKeyFile} \ + -out ${cfg.dataDir}/certs/transport_key.pem + chown -R omega:omega ${cfg.dataDir} + '') + ]; + + 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; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.transportPort cfg.apiPort ]; + allowedUDPPorts = [ cfg.transportPort cfg.apiPort ]; + }; + }; + }; + }; + }; +} diff --git a/src/main.rs b/src/main.rs index a6b127a..67f8bb1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,8 +37,13 @@ async fn main() { log_in!("Incoming messages"); log_out!("Outgoing messages"); + let omikron_port: u16 = env::var("OMIKRON_PORT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(9187); + tokio::spawn(async move { - match omikron_connection::start(9187).await { + match omikron_connection::start(omikron_port).await { Err(e) => log_err!(0, PrintType::General, "{:?}", e), _ => {} } @@ -68,7 +73,12 @@ async fn main() { log!(" Users"); } - let _ = server::server::start(9188).await; + let api_port: u16 = env::var("API_PORT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(9188); + + let _ = server::server::start(api_port).await; tokio::signal::ctrl_c().await.unwrap(); } diff --git a/src/server/server.rs b/src/server/server.rs index 1fcfe04..3c7fc05 100644 --- a/src/server/server.rs +++ b/src/server/server.rs @@ -30,7 +30,8 @@ pub async fn start(port: u16) -> anyhow::Result<()> { config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - let addr = format!("0.0.0.0:{port}"); + let bind_addr = std::env::var("BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0".to_string()); + let addr = format!("{}:{}", bind_addr, port); log!(" Server on {}", addr); HttpServer::new(move || { diff --git a/src/util/file_util.rs b/src/util/file_util.rs index b108810..f1c51db 100644 --- a/src/util/file_util.rs +++ b/src/util/file_util.rs @@ -163,9 +163,8 @@ pub fn get_children(path: &str) -> Vec { } pub fn get_directory() -> String { - let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from(".")); - exe.parent() - .unwrap_or(Path::new(".")) + std::env::current_dir() + .unwrap_or_else(|_| PathBuf::from(".")) .to_string_lossy() .to_string() }