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

View file

@ -60,4 +60,5 @@ usermod -aG iota-operators USER
```
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];
in {
packages = {
default = self'.packages.iota-daemon;
iota-daemon = pkgs.rustPlatform.buildRustPackage {
pname = "iota-daemon";
default = pkgs.rustPlatform.buildRustPackage {
pname = "iota";
version = "0.1.0";
src = ./.;
cargoBuildFlags = ["-p" "iota-daemon"];
cargoBuildFlags = ["-p" "iota" "-p" "iota-daemon"];
cargoLock = {
lockFile = ./Cargo.lock;
allowBuiltinFetchGit = true;
@ -56,6 +54,12 @@
nativeBuildInputs = commonNativeBuildInputs;
buildInputs = commonBuildInputs;
dontUseCmakeConfigure = true;
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
@ -63,33 +67,22 @@
fi
done
'';
passthru.dataDir = "/var/lib/iota";
};
});
iota-ui = pkgs.rustPlatform.buildRustPackage {
iota-ui = self'.packages.default.overrideAttrs (old: {
pname = "iota-ui";
version = "0.1.0";
src = ./.;
cargoBuildFlags = ["-p" "iota"];
cargoLock = {
lockFile = ./Cargo.lock;
allowBuiltinFetchGit = true;
};
nativeBuildInputs = commonNativeBuildInputs;
buildInputs = commonBuildInputs;
dontUseCmakeConfigure = true;
postInstall = ''
for f in $out/bin/*; do
if [ "$(basename "$f")" != "iota" ]; then
rm "$f"
fi
done
# Rename to avoid confusion
if [ -f "$out/bin/iota" ]; then
mv "$out/bin/iota" "$out/bin/iota-ui"
fi
'';
};
});
};
devShells.default = pkgs.mkShell {
@ -106,7 +99,7 @@
...
}: let
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 {};
configFile =
@ -190,7 +183,7 @@
users.groups.iota = {};
systemd.sockets.iota-daemon = {
systemd.sockets.iota = {
description = "${descriptionText} IPC socket";
wantedBy = ["sockets.target"];
socketConfig = {
@ -205,10 +198,11 @@
};
};
systemd.services.iota-daemon = {
systemd.services.iota = {
description = descriptionText;
after = ["network.target"];
requires = ["iota-daemon.socket"];
wantedBy = ["multi-user.target"];
after = ["network.target" "iota.socket"];
requires = ["iota.socket"];
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)]
pub enum PathError {
MissingPlatformDirectory(&'static str),
MissingRequiredOverride(&'static str),
EmptyOverride(&'static str),
RelativeOverride {
variable: &'static str,
@ -37,6 +38,7 @@ impl fmt::Display for PathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
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::RelativeOverride { variable, value } => {
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 config_file = override_first(&["IOTA_CONFIG_FILE"])?
.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 {
scope,
config_dir,
@ -234,7 +240,7 @@ struct Defaults {
log_dir: PathBuf,
asset_dir: PathBuf,
install_root: PathBuf,
ipc_endpoint: IpcEndpoint,
ipc_endpoint: Option<IpcEndpoint>,
}
impl Defaults {
fn for_scope(scope: Scope) -> Result<Self, PathError> {
@ -250,7 +256,7 @@ impl Defaults {
log_dir: "/var/log/iota".into(),
asset_dir: "/usr/local/share/iota/web".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"))]
@ -271,18 +277,15 @@ fn user_defaults() -> Result<Defaults, PathError> {
let state_base = xdg_or_home("XDG_STATE_HOME", &home, ".local/state")?;
let cache_base = xdg_or_home("XDG_CACHE_HOME", &home, ".cache")?;
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 {
config_dir: config_base.join("iota"),
state_dir: state_base.join("iota"),
cache_dir: cache_base.join("iota"),
runtime_dir: Some(runtime.clone()),
runtime_dir: None,
log_dir: state_base.join("iota/logs"),
asset_dir: data_base.join("iota/web"),
install_root: data_base.join("iota/bin"),
ipc_endpoint: IpcEndpoint::UnixSocket(runtime.join("iota.sock")),
ipc_endpoint: None,
})
}
#[cfg(windows)]
@ -301,7 +304,9 @@ fn user_defaults() -> Result<Defaults, PathError> {
log_dir: local.join("logs"),
asset_dir: local.join("data"),
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)
}
fn resolve_ipc(
scope: Scope,
runtime: Option<&Path>,
default: IpcEndpoint,
) -> Result<IpcEndpoint, PathError> {
fn resolve_ipc(scope: Scope, default: Option<IpcEndpoint>) -> Result<IpcEndpoint, PathError> {
#[cfg(unix)]
{
if let Some(path) = absolute_env("IOTA_SOCKET")? {
return Ok(IpcEndpoint::UnixSocket(path));
}
if scope == Scope::User {
return runtime
.map(|dir| IpcEndpoint::UnixSocket(dir.join("iota.sock")))
.ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR"));
return Err(PathError::MissingRequiredOverride("IOTA_SOCKET"));
}
}
#[cfg(windows)]
@ -358,7 +357,7 @@ fn resolve_ipc(
return Ok(IpcEndpoint::WindowsPipe(name));
}
}
Ok(default)
default.ok_or(PathError::UnsupportedScope)
}
fn create_directory(path: &Path, private: bool) -> std::io::Result<()> {
std::fs::create_dir_all(path)?;

View file

@ -218,6 +218,8 @@ mod systemd {
pub struct SystemdManager {
executor: Arc<dyn CommandExecutor>,
service: &'static str,
socket: &'static str,
}
impl SystemdManager {
pub async fn detect() -> Option<Self> {
@ -225,16 +227,29 @@ mod systemd {
return None;
}
let executor: Arc<dyn CommandExecutor> = Arc::new(RealExecutor);
executor
let mut manager = executor
.output("systemctl", &["--version", &COMMON[0], &COMMON[1]])
.await
.ok()
.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)]
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> {
let mut args = COMMON.to_vec();
@ -358,19 +373,25 @@ mod systemd {
async fn unit_status(&self, unit: &str) -> Result<UnitStatus, ProcessManagerError> {
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(
&self,
mode: StartupMode,
) -> Result<DaemonStartupStatus, ProcessManagerError> {
match mode {
StartupMode::AlwaysOn => {
self.run(&["disable", SOCKET]).await?;
self.run(&["enable", "--now", SERVICE]).await?;
self.run(&["disable", self.socket]).await?;
self.run(&["enable", "--now", self.service]).await?;
self.verify(DetectedStartupMode::AlwaysOn).await
}
StartupMode::SocketActivated => {
self.run(&["disable", "--now", SERVICE]).await?;
self.run(&["enable", "--now", SOCKET]).await?;
self.run(&["disable", "--now", self.service]).await?;
self.run(&["enable", "--now", self.socket]).await?;
self.verify(DetectedStartupMode::SocketActivated).await
}
}
@ -378,9 +399,21 @@ mod systemd {
async fn unit_action(&self, action: &[&str]) -> Result<(), ProcessManagerError> {
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> {
self.run(&["disable", "--now", SERVICE]).await?;
self.run(&["disable", "--now", SOCKET]).await?;
self.run(&["disable", "--now", self.service]).await?;
self.run(&["disable", "--now", self.socket]).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());
}
}