Merge branch 'main' of ssh://git.methanium.net/tensamin/iota

This commit is contained in:
Alex 2026-07-25 22:59:35 +02:00
commit 6b4c144b73
Signed by: alex
SSH key fingerprint: SHA256:D1+Ub8o0v4K5y1JNivW8IxEOelqLSvPmUzBbDIoZkRQ
7 changed files with 92 additions and 130 deletions

25
LICENSE
View file

@ -1,16 +1,15 @@
Copyright (c) [2025] [Methanium] Copyright (c) 2025 Methanium
All rights reserved. All rights reserved.
This software is protected by copyright. Copying, editing, No part of this software, source code, documentation, or
distributing, publicly performing, or any other use of this software associated materials may be copied, reproduced, modified,
or its components, in source or binary form, is strictly prohibited without the express distributed, published, sublicensed, sold, or used to create
written permission of the copyright holder. derivative works without prior written permission from the
copyright holder.
FUTURE LICENSE ACCEPTANCE: THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY
It is the copyright holder's intention to release this software in the future OF ANY KIND, EXPRESS OR IMPLIED. TO THE MAXIMUM
under a license yet to be defined, which will, among other things, EXTENT PERMITTED BY LAW, THE COPYRIGHT HOLDER SHALL
allow private, non-commercial use. This statement does not constitute NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
a current license grant and does not alter the above LIABILITY ARISING FROM THE SOFTWARE OR ITS USE.
prohibition on use, copying, or modification. Until the formal
publication of such a future license, all rights remain
reserved.

View file

@ -60,4 +60,5 @@ usermod -aG iota-operators USER
``` ```
The user must start a new login session before supplementary group membership The user must start a new login session before supplementary group membership
is visible. `IOTA_SOCKET` remains authoritative for custom deployments. is visible. Unix per-user deployments must set `IOTA_SOCKET` to an absolute
path; Iota does not derive its IPC socket from `XDG_RUNTIME_DIR`.

View file

@ -42,13 +42,11 @@
commonNativeBuildInputs = with pkgs; [cmake perl pkg-config]; commonNativeBuildInputs = with pkgs; [cmake perl pkg-config];
in { in {
packages = { packages = {
default = self'.packages.iota-daemon; default = pkgs.rustPlatform.buildRustPackage {
pname = "iota";
iota-daemon = pkgs.rustPlatform.buildRustPackage {
pname = "iota-daemon";
version = "0.1.0"; version = "0.1.0";
src = ./.; src = ./.;
cargoBuildFlags = ["-p" "iota-daemon"]; cargoBuildFlags = ["-p" "iota" "-p" "iota-daemon"];
cargoLock = { cargoLock = {
lockFile = ./Cargo.lock; lockFile = ./Cargo.lock;
allowBuiltinFetchGit = true; allowBuiltinFetchGit = true;
@ -56,6 +54,12 @@
nativeBuildInputs = commonNativeBuildInputs; nativeBuildInputs = commonNativeBuildInputs;
buildInputs = commonBuildInputs; buildInputs = commonBuildInputs;
dontUseCmakeConfigure = true; dontUseCmakeConfigure = true;
passthru.dataDir = "/var/lib/iota";
};
iota-daemon = self'.packages.default.overrideAttrs (old: {
pname = "iota-daemon";
cargoBuildFlags = ["-p" "iota-daemon"];
postInstall = '' postInstall = ''
for f in $out/bin/*; do for f in $out/bin/*; do
if [ "$(basename "$f")" != "iota-daemon" ]; then if [ "$(basename "$f")" != "iota-daemon" ]; then
@ -63,33 +67,22 @@
fi fi
done done
''; '';
passthru.dataDir = "/var/lib/iota"; });
};
iota-ui = pkgs.rustPlatform.buildRustPackage { iota-ui = self'.packages.default.overrideAttrs (old: {
pname = "iota-ui"; pname = "iota-ui";
version = "0.1.0";
src = ./.;
cargoBuildFlags = ["-p" "iota"]; cargoBuildFlags = ["-p" "iota"];
cargoLock = {
lockFile = ./Cargo.lock;
allowBuiltinFetchGit = true;
};
nativeBuildInputs = commonNativeBuildInputs;
buildInputs = commonBuildInputs;
dontUseCmakeConfigure = true;
postInstall = '' postInstall = ''
for f in $out/bin/*; do for f in $out/bin/*; do
if [ "$(basename "$f")" != "iota" ]; then if [ "$(basename "$f")" != "iota" ]; then
rm "$f" rm "$f"
fi fi
done done
# Rename to avoid confusion
if [ -f "$out/bin/iota" ]; then if [ -f "$out/bin/iota" ]; then
mv "$out/bin/iota" "$out/bin/iota-ui" mv "$out/bin/iota" "$out/bin/iota-ui"
fi fi
''; '';
}; });
}; };
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
@ -106,7 +99,7 @@
... ...
}: let }: let
cfg = config.services.iota; cfg = config.services.iota;
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.iota-daemon or (throw "iota: no pre-built package for system ${pkgs.stdenv.hostPlatform.system}"); 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 {}; configFormat = pkgs.formats.yaml {};
configFile = configFile =
@ -190,7 +183,7 @@
users.groups.iota = {}; users.groups.iota = {};
systemd.sockets.iota-daemon = { systemd.sockets.iota = {
description = "${descriptionText} IPC socket"; description = "${descriptionText} IPC socket";
wantedBy = ["sockets.target"]; wantedBy = ["sockets.target"];
socketConfig = { socketConfig = {
@ -205,10 +198,11 @@
}; };
}; };
systemd.services.iota-daemon = { systemd.services.iota = {
description = descriptionText; description = descriptionText;
after = ["network.target"]; wantedBy = ["multi-user.target"];
requires = ["iota-daemon.socket"]; after = ["network.target" "iota.socket"];
requires = ["iota.socket"];
serviceConfig = serviceConfig =
{ {

View file

@ -172,20 +172,4 @@ fn run(program: &str, args: &[&str]) -> Result<()> {
} }
} }
#[cfg(test)]
mod tests {
#[test]
fn service_uses_installed_daemon_and_declared_identities() {
let service = include_str!("../../systemd/iota-daemon.service");
let socket = include_str!("../../systemd/iota-daemon.socket");
let sysusers = include_str!("../../systemd/sysusers.d/iota.conf");
assert!(service.contains("ExecStart=/usr/local/libexec/iota/iota-daemon"));
assert!(service.contains("User=iota"));
assert!(service.contains("Group=iota"));
assert!(socket.contains("SocketUser=iota"));
assert!(socket.contains("SocketGroup=iota-operators"));
assert!(socket.contains("NonBlocking=true"));
assert!(sysusers.contains("u iota "));
assert!(sysusers.contains("g iota-operators"));
}
}

View file

@ -25,6 +25,7 @@ pub enum IpcEndpoint {
#[derive(Debug)] #[derive(Debug)]
pub enum PathError { pub enum PathError {
MissingPlatformDirectory(&'static str), MissingPlatformDirectory(&'static str),
MissingRequiredOverride(&'static str),
EmptyOverride(&'static str), EmptyOverride(&'static str),
RelativeOverride { RelativeOverride {
variable: &'static str, variable: &'static str,
@ -37,6 +38,7 @@ impl fmt::Display for PathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::MissingPlatformDirectory(name) => write!(f, "missing platform directory: {name}"), Self::MissingPlatformDirectory(name) => write!(f, "missing platform directory: {name}"),
Self::MissingRequiredOverride(name) => write!(f, "{name} must be set"),
Self::EmptyOverride(name) => write!(f, "{name} must not be empty"), Self::EmptyOverride(name) => write!(f, "{name} must not be empty"),
Self::RelativeOverride { variable, value } => { Self::RelativeOverride { variable, value } => {
write!(f, "{variable} must be absolute, got {}", value.display()) write!(f, "{variable} must be absolute, got {}", value.display())
@ -81,7 +83,11 @@ impl IotaPaths {
let install_root = override_first(&["IOTA_INSTALL_ROOT"])?.unwrap_or(defaults.install_root); let install_root = override_first(&["IOTA_INSTALL_ROOT"])?.unwrap_or(defaults.install_root);
let config_file = override_first(&["IOTA_CONFIG_FILE"])? let config_file = override_first(&["IOTA_CONFIG_FILE"])?
.unwrap_or_else(|| config_dir.join("config.yaml")); .unwrap_or_else(|| config_dir.join("config.yaml"));
let ipc_endpoint = resolve_ipc(scope, runtime_dir.as_deref(), defaults.ipc_endpoint)?; let ipc_endpoint = resolve_ipc(scope, defaults.ipc_endpoint)?;
let runtime_dir = runtime_dir.or_else(|| match &ipc_endpoint {
IpcEndpoint::UnixSocket(path) => path.parent().map(Path::to_path_buf),
IpcEndpoint::WindowsPipe(_) => None,
});
Ok(Self { Ok(Self {
scope, scope,
config_dir, config_dir,
@ -234,7 +240,7 @@ struct Defaults {
log_dir: PathBuf, log_dir: PathBuf,
asset_dir: PathBuf, asset_dir: PathBuf,
install_root: PathBuf, install_root: PathBuf,
ipc_endpoint: IpcEndpoint, ipc_endpoint: Option<IpcEndpoint>,
} }
impl Defaults { impl Defaults {
fn for_scope(scope: Scope) -> Result<Self, PathError> { fn for_scope(scope: Scope) -> Result<Self, PathError> {
@ -250,7 +256,7 @@ impl Defaults {
log_dir: "/var/log/iota".into(), log_dir: "/var/log/iota".into(),
asset_dir: "/usr/local/share/iota/web".into(), asset_dir: "/usr/local/share/iota/web".into(),
install_root: "/usr/local/libexec/iota".into(), install_root: "/usr/local/libexec/iota".into(),
ipc_endpoint: IpcEndpoint::UnixSocket("/run/iota/iota.sock".into()), ipc_endpoint: Some(IpcEndpoint::UnixSocket("/run/iota/iota.sock".into())),
}) })
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
@ -271,18 +277,15 @@ fn user_defaults() -> Result<Defaults, PathError> {
let state_base = xdg_or_home("XDG_STATE_HOME", &home, ".local/state")?; let state_base = xdg_or_home("XDG_STATE_HOME", &home, ".local/state")?;
let cache_base = xdg_or_home("XDG_CACHE_HOME", &home, ".cache")?; let cache_base = xdg_or_home("XDG_CACHE_HOME", &home, ".cache")?;
let data_base = xdg_or_home("XDG_DATA_HOME", &home, ".local/share")?; let data_base = xdg_or_home("XDG_DATA_HOME", &home, ".local/share")?;
let runtime = absolute_env("XDG_RUNTIME_DIR")?
.ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR"))?
.join("iota");
Ok(Defaults { Ok(Defaults {
config_dir: config_base.join("iota"), config_dir: config_base.join("iota"),
state_dir: state_base.join("iota"), state_dir: state_base.join("iota"),
cache_dir: cache_base.join("iota"), cache_dir: cache_base.join("iota"),
runtime_dir: Some(runtime.clone()), runtime_dir: None,
log_dir: state_base.join("iota/logs"), log_dir: state_base.join("iota/logs"),
asset_dir: data_base.join("iota/web"), asset_dir: data_base.join("iota/web"),
install_root: data_base.join("iota/bin"), install_root: data_base.join("iota/bin"),
ipc_endpoint: IpcEndpoint::UnixSocket(runtime.join("iota.sock")), ipc_endpoint: None,
}) })
} }
#[cfg(windows)] #[cfg(windows)]
@ -301,7 +304,9 @@ fn user_defaults() -> Result<Defaults, PathError> {
log_dir: local.join("logs"), log_dir: local.join("logs"),
asset_dir: local.join("data"), asset_dir: local.join("data"),
install_root: local.join("bin"), install_root: local.join("bin"),
ipc_endpoint: IpcEndpoint::WindowsPipe(r"\\.\pipe\Tensamin.Iota.User".into()), ipc_endpoint: Some(IpcEndpoint::WindowsPipe(
r"\\.\pipe\Tensamin.Iota.User".into(),
)),
}) })
} }
@ -332,20 +337,14 @@ fn override_first(names: &[&'static str]) -> Result<Option<PathBuf>, PathError>
} }
Ok(None) Ok(None)
} }
fn resolve_ipc( fn resolve_ipc(scope: Scope, default: Option<IpcEndpoint>) -> Result<IpcEndpoint, PathError> {
scope: Scope,
runtime: Option<&Path>,
default: IpcEndpoint,
) -> Result<IpcEndpoint, PathError> {
#[cfg(unix)] #[cfg(unix)]
{ {
if let Some(path) = absolute_env("IOTA_SOCKET")? { if let Some(path) = absolute_env("IOTA_SOCKET")? {
return Ok(IpcEndpoint::UnixSocket(path)); return Ok(IpcEndpoint::UnixSocket(path));
} }
if scope == Scope::User { if scope == Scope::User {
return runtime return Err(PathError::MissingRequiredOverride("IOTA_SOCKET"));
.map(|dir| IpcEndpoint::UnixSocket(dir.join("iota.sock")))
.ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR"));
} }
} }
#[cfg(windows)] #[cfg(windows)]
@ -358,7 +357,7 @@ fn resolve_ipc(
return Ok(IpcEndpoint::WindowsPipe(name)); return Ok(IpcEndpoint::WindowsPipe(name));
} }
} }
Ok(default) default.ok_or(PathError::UnsupportedScope)
} }
fn create_directory(path: &Path, private: bool) -> std::io::Result<()> { fn create_directory(path: &Path, private: bool) -> std::io::Result<()> {
std::fs::create_dir_all(path)?; std::fs::create_dir_all(path)?;

View file

@ -218,6 +218,8 @@ mod systemd {
pub struct SystemdManager { pub struct SystemdManager {
executor: Arc<dyn CommandExecutor>, executor: Arc<dyn CommandExecutor>,
service: &'static str,
socket: &'static str,
} }
impl SystemdManager { impl SystemdManager {
pub async fn detect() -> Option<Self> { pub async fn detect() -> Option<Self> {
@ -225,16 +227,29 @@ mod systemd {
return None; return None;
} }
let executor: Arc<dyn CommandExecutor> = Arc::new(RealExecutor); let executor: Arc<dyn CommandExecutor> = Arc::new(RealExecutor);
executor let mut manager = executor
.output("systemctl", &["--version", &COMMON[0], &COMMON[1]]) .output("systemctl", &["--version", &COMMON[0], &COMMON[1]])
.await .await
.ok() .ok()
.filter(|r| r.success) .filter(|r| r.success)
.map(|_| Self { executor }) .map(|_| Self {
executor,
service: SERVICE,
socket: SOCKET,
})?;
if manager.status("iota.service").await.is_ok() {
manager.service = "iota.service";
manager.socket = "iota.socket";
}
Some(manager)
} }
#[cfg(test)] #[cfg(test)]
pub fn with_executor(executor: Arc<dyn CommandExecutor>) -> Self { pub fn with_executor(executor: Arc<dyn CommandExecutor>) -> Self {
Self { executor } Self {
executor,
service: SERVICE,
socket: SOCKET,
}
} }
async fn run(&self, action: &[&str]) -> Result<(), ProcessManagerError> { async fn run(&self, action: &[&str]) -> Result<(), ProcessManagerError> {
let mut args = COMMON.to_vec(); let mut args = COMMON.to_vec();
@ -358,19 +373,25 @@ mod systemd {
async fn unit_status(&self, unit: &str) -> Result<UnitStatus, ProcessManagerError> { async fn unit_status(&self, unit: &str) -> Result<UnitStatus, ProcessManagerError> {
self.status(unit).await self.status(unit).await
} }
async fn iota_startup_status(&self) -> Result<DaemonStartupStatus, ProcessManagerError> {
Ok(DaemonStartupStatus::classify(
self.status(self.service).await?,
self.status(self.socket).await?,
))
}
async fn set_iota_startup_mode( async fn set_iota_startup_mode(
&self, &self,
mode: StartupMode, mode: StartupMode,
) -> Result<DaemonStartupStatus, ProcessManagerError> { ) -> Result<DaemonStartupStatus, ProcessManagerError> {
match mode { match mode {
StartupMode::AlwaysOn => { StartupMode::AlwaysOn => {
self.run(&["disable", SOCKET]).await?; self.run(&["disable", self.socket]).await?;
self.run(&["enable", "--now", SERVICE]).await?; self.run(&["enable", "--now", self.service]).await?;
self.verify(DetectedStartupMode::AlwaysOn).await self.verify(DetectedStartupMode::AlwaysOn).await
} }
StartupMode::SocketActivated => { StartupMode::SocketActivated => {
self.run(&["disable", "--now", SERVICE]).await?; self.run(&["disable", "--now", self.service]).await?;
self.run(&["enable", "--now", SOCKET]).await?; self.run(&["enable", "--now", self.socket]).await?;
self.verify(DetectedStartupMode::SocketActivated).await self.verify(DetectedStartupMode::SocketActivated).await
} }
} }
@ -378,9 +399,21 @@ mod systemd {
async fn unit_action(&self, action: &[&str]) -> Result<(), ProcessManagerError> { async fn unit_action(&self, action: &[&str]) -> Result<(), ProcessManagerError> {
self.run(action).await self.run(action).await
} }
async fn process_action(
&self,
action: ProcessAction,
) -> Result<DaemonStartupStatus, ProcessManagerError> {
let verb = match action {
ProcessAction::Start => "start",
ProcessAction::Stop => "stop",
ProcessAction::Restart => "restart",
};
self.run(&[verb, self.service]).await?;
self.iota_startup_status().await
}
async fn disable_startup(&self) -> Result<DaemonStartupStatus, ProcessManagerError> { async fn disable_startup(&self) -> Result<DaemonStartupStatus, ProcessManagerError> {
self.run(&["disable", "--now", SERVICE]).await?; self.run(&["disable", "--now", self.service]).await?;
self.run(&["disable", "--now", SOCKET]).await?; self.run(&["disable", "--now", self.socket]).await?;
self.verify(DetectedStartupMode::Disabled).await self.verify(DetectedStartupMode::Disabled).await
} }
} }

View file

@ -240,52 +240,4 @@ fn pending_forward_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Pending
}) })
} }
#[cfg(test)]
mod tests {
use super::*;
fn suffix(name: &str) -> String {
let unix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
format!("{name}-{unix}")
}
#[test]
fn stores_and_retrieves_chat_secret_blob_for_owner_chat() {
let user_id = suffix("user");
let chat_id = suffix("chat");
put_chat_secret(StoredChatSecret {
user_id: user_id.clone(),
chat_id: chat_id.clone(),
secret_id: "main".to_string(),
version: 1,
encrypted_secret: vec![42, 43],
kem_ciphertext: vec![9, 8, 7],
wrapping_scheme: "mtp-kem-chacha20poly1305-hkdf-sha256-v1".to_string(),
created_at: 1,
updated_at: 2,
})
.unwrap();
let found = get_chat_secret(ChatSecretQuery {
user_id: user_id.clone(),
chat_id: chat_id.clone(),
secret_id: Some("main".to_string()),
})
.unwrap()
.unwrap();
assert_eq!(found.encrypted_secret, vec![42, 43]);
assert_eq!(found.kem_ciphertext, vec![9, 8, 7]);
let denied = get_chat_secret(ChatSecretQuery {
user_id: suffix("other-user"),
chat_id,
secret_id: Some("main".to_string()),
})
.unwrap();
assert!(denied.is_none());
}
}