Persistencia setters, compat-resolved, journald→CAS, compat-polkit
- hostnamed: SetHostname llama sethostname(2) + cache. SetStaticHostname
escribe atómico a /etc/hostname (tmp + fsync + rename, perms 0644).
Set{Pretty,Icon,Chassis,Deployment,Location} mergean k=v en
/etc/machine-info preservando otras keys. Validación: hostname RFC 1123
+ chassis enum.
- timedated: SetTimezone valida que /usr/share/zoneinfo/<tz> exista,
hace symlink atómico a /etc/localtime.
- localed: SetLocale valida formato KEY=value, escribe a /etc/locale.conf.
- compat-resolved (nuevo): org.freedesktop.resolve1.Manager con
ResolveHostname (vía tokio::lookup_host) y ResolveAddress (getnameinfo).
ResolveRecord devuelve NotSupported.
- journald-compat: persiste cada datagram al CAS por SHA. Append a
~/.local/share/ente/journal/index.log con
timestamp_ms:source:unit:sha_hex. Mutex serializa escrituras.
- compat-polkit (nuevo): org.freedesktop.PolicyKit1.Authority always-yes.
CheckAuthorization/CheckAuthorizationByAsync responden true,false,{}.
EnumerateActions vacío. Loguea action_id + pid/uid del subject.
7 compat-shims operativos en paralelo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d88a9c57914ac7870b43a7a4f616510161278237 · por Sergio · (hace 5 meses) · padre 6ad6d08fa · ver el árbol en este commit
Delta semántico · 59
crates/ente-hostnamed-compat/src/main.rs
crates/ente-journald-compat/src/main.rs
crates/ente-localed-compat/src/main.rs
crates/ente-polkit-compat/src/main.rs
crates/ente-resolved-compat/src/main.rs
crates/ente-timedated-compat/src/main.rs
crates/ente-zero/src/seed.rs
diff --git a/Cargo.lock b/Cargo.lock
index 91d02b8..821713e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -400,6 +400,7 @@ dependencies = [
"anyhow",
"ente-bus",
"ente-card",
+ "ente-cas",
"libc",
"nix",
"tokio",
@@ -445,6 +446,33 @@ dependencies = [
"zbus",
]
+[[package]]
+name = "ente-polkit-compat"
+version = "0.0.1"
+dependencies = [
+ "anyhow",
+ "ente-bus",
+ "ente-card",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+ "zbus",
+]
+
+[[package]]
+name = "ente-resolved-compat"
+version = "0.0.1"
+dependencies = [
+ "anyhow",
+ "ente-bus",
+ "ente-card",
+ "libc",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+ "zbus",
+]
+
[[package]]
name = "ente-snapshot"
version = "0.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index 3f02310..8683fd4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,6 +16,8 @@ members = [
"crates/ente-timedated-compat",
"crates/ente-localed-compat",
"crates/ente-journald-compat",
+ "crates/ente-resolved-compat",
+ "crates/ente-polkit-compat",
]
[workspace.package]
diff --git a/crates/ente-hostnamed-compat/src/main.rs b/crates/ente-hostnamed-compat/src/main.rs
index 4f78e83..ece91a7 100644
--- a/crates/ente-hostnamed-compat/src/main.rs
+++ b/crates/ente-hostnamed-compat/src/main.rs
@@ -154,38 +154,67 @@ impl HostnameManager {
// ----- Setters: forward al bus interno y guardan en cache -----
async fn set_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> {
- info!(%name, "SetHostname → bus interno (stub)");
- *self.transient_hostname.lock().unwrap() = Some(name);
+ if !is_valid_hostname(&name) {
+ return Err(fdo::Error::InvalidArgs(format!("hostname inválido: {name:?}")));
+ }
+ // sethostname(2) cambia sólo el running kernel value.
+ let cstr = std::ffi::CString::new(name.clone())
+ .map_err(|e| fdo::Error::Failed(format!("CString: {e}")))?;
+ let r = unsafe { libc::sethostname(cstr.as_ptr(), name.len()) };
+ if r != 0 {
+ warn!(error = %std::io::Error::last_os_error(), %name, "sethostname syscall falló (¿CAP_SYS_ADMIN?)");
+ // No es fatal — guardamos transient para que el property lea el valor nuevo.
+ }
+ *self.transient_hostname.lock().unwrap() = Some(name.clone());
+ info!(%name, "SetHostname aplicado");
Ok(())
}
async fn set_static_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> {
- info!(%name, "SetStaticHostname (stub: no persistimos a /etc)");
+ if !is_valid_hostname(&name) {
+ return Err(fdo::Error::InvalidArgs(format!("hostname inválido: {name:?}")));
+ }
+ atomic_write("/etc/hostname", format!("{name}\n").as_bytes())
+ .map_err(|e| fdo::Error::Failed(format!("write /etc/hostname: {e}")))?;
+ info!(%name, "SetStaticHostname → /etc/hostname");
Ok(())
}
async fn set_pretty_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> {
- info!(%name, "SetPrettyHostname (stub)");
+ update_machine_info("PRETTY_HOSTNAME", &name)
+ .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
+ info!(%name, "SetPrettyHostname → /etc/machine-info");
Ok(())
}
async fn set_icon_name(&self, name: String, _interactive: bool) -> fdo::Result<()> {
- info!(%name, "SetIconName (stub)");
+ update_machine_info("ICON_NAME", &name)
+ .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
+ info!(%name, "SetIconName → /etc/machine-info");
Ok(())
}
async fn set_chassis(&self, chassis: String, _interactive: bool) -> fdo::Result<()> {
- info!(%chassis, "SetChassis (stub)");
+ if !matches!(chassis.as_str(), "desktop"|"laptop"|"server"|"tablet"|"handset"|"watch"|"embedded"|"vm"|"container") {
+ return Err(fdo::Error::InvalidArgs(format!("chassis inválido: {chassis}")));
+ }
+ update_machine_info("CHASSIS", &chassis)
+ .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
+ info!(%chassis, "SetChassis → /etc/machine-info");
Ok(())
}
async fn set_deployment(&self, deployment: String, _interactive: bool) -> fdo::Result<()> {
- info!(%deployment, "SetDeployment (stub)");
+ update_machine_info("DEPLOYMENT", &deployment)
+ .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
+ info!(%deployment, "SetDeployment → /etc/machine-info");
Ok(())
}
async fn set_location(&self, location: String, _interactive: bool) -> fdo::Result<()> {
- info!(%location, "SetLocation (stub)");
+ update_machine_info("LOCATION", &location)
+ .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
+ info!(%location, "SetLocation → /etc/machine-info");
Ok(())
}
}
@@ -226,6 +255,58 @@ fn read_dmi(path: &str) -> String {
.unwrap_or_default()
}
+/// RFC 1123 + extra: ASCII alfanumérico, dash, dot. Longitud 1..253.
+/// Rechaza vacíos, espacios, control chars.
+fn is_valid_hostname(s: &str) -> bool {
+ if s.is_empty() || s.len() > 253 { return false; }
+ s.chars().all(|c|
+ c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_'
+ )
+}
+
+/// Escritura atómica via tmp + rename. fsync del directorio para
+/// garantizar durabilidad post-crash. Permisos 0644.
+fn atomic_write(path: &str, content: &[u8]) -> std::io::Result<()> {
+ use std::io::Write;
+ use std::os::unix::fs::OpenOptionsExt;
+ let p = std::path::Path::new(path);
+ if let Some(parent) = p.parent() { let _ = std::fs::create_dir_all(parent); }
+ let tmp = p.with_extension("tmp");
+ {
+ let mut f = std::fs::OpenOptions::new()
+ .create(true).write(true).truncate(true)
+ .mode(0o644)
+ .open(&tmp)?;
+ f.write_all(content)?;
+ f.sync_all()?;
+ }
+ std::fs::rename(&tmp, p)?;
+ Ok(())
+}
+
+/// Lee /etc/machine-info, actualiza/inserta una clave, escribe atómico.
+fn update_machine_info(key: &str, value: &str) -> std::io::Result<()> {
+ let path = "/etc/machine-info";
+ let existing = std::fs::read_to_string(path).unwrap_or_default();
+ let mut found = false;
+ let mut out = String::new();
+ for line in existing.lines() {
+ if let Some((k, _)) = line.split_once('=') {
+ if k.trim() == key {
+ out.push_str(&format!("{key}={value}\n"));
+ found = true;
+ continue;
+ }
+ }
+ out.push_str(line);
+ out.push('\n');
+ }
+ if !found {
+ out.push_str(&format!("{key}={value}\n"));
+ }
+ atomic_write(path, out.as_bytes())
+}
+
async fn announce_to_fractal() {
if let Ok(mut client) = BusClient::from_env().await {
let req = BusRequest::Announce {
diff --git a/crates/ente-journald-compat/Cargo.toml b/crates/ente-journald-compat/Cargo.toml
index 4b74f11..9c8fc1f 100644
--- a/crates/ente-journald-compat/Cargo.toml
+++ b/crates/ente-journald-compat/Cargo.toml
@@ -12,6 +12,7 @@ path = "src/main.rs"
[dependencies]
ente-card = { path = "../ente-card" }
ente-bus = { path = "../ente-bus" }
+ente-cas = { path = "../ente-cas" }
nix = { workspace = true }
libc = { workspace = true }
anyhow = { workspace = true }
diff --git a/crates/ente-journald-compat/src/main.rs b/crates/ente-journald-compat/src/main.rs
index 3da7297..620cfd2 100644
--- a/crates/ente-journald-compat/src/main.rs
+++ b/crates/ente-journald-compat/src/main.rs
@@ -10,7 +10,8 @@
use ente_bus::{BusClient, BusRequest, BusResponse};
use ente_card::Capability;
use std::os::fd::{AsRawFd, OwnedFd};
-use std::path::Path;
+use std::path::{Path, PathBuf};
+use std::sync::Mutex;
use tokio::io::unix::AsyncFd;
use tokio::signal::unix::{signal, SignalKind};
use tracing::{debug, info, warn};
@@ -102,16 +103,65 @@ fn spawn_listener(async_fd: AsyncFd<OwnedFdWrap>, source: &'static str) {
});
}
+/// Mutex sobre el archivo index para escrituras concurrentes desde
+/// múltiples listeners (journal + syslog).
+static INDEX_FILE: Mutex<()> = Mutex::new(());
+
+/// Path del index file: `$XDG_DATA_HOME/ente/journal/index.log` (default
+/// `~/.local/share/ente/journal/index.log`).
+fn index_path() -> PathBuf {
+ let base = if let Ok(d) = std::env::var("XDG_DATA_HOME") { d }
+ else if let Ok(h) = std::env::var("HOME") { format!("{h}/.local/share") }
+ else { "/var/lib".into() };
+ PathBuf::from(base).join("ente").join("journal").join("index.log")
+}
+
+fn now_ms() -> u128 {
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .map(|d| d.as_millis())
+ .unwrap_or(0)
+}
+
+/// Persiste el blob crudo al CAS y appendea una línea al index:
+/// `<timestamp_ms>:<source>:<unit>:<sha_hex>`. Errores se logean pero
+/// no abortan — perder un mensaje no debe romper journald.
+fn persist_to_cas(buf: &[u8], source: &'static str, unit: Option<&str>) {
+ let sha = match ente_cas::store(buf) {
+ Ok(s) => s,
+ Err(e) => { warn!(?e, "CAS store falló"); return; }
+ };
+ let line = format!(
+ "{}:{}:{}:{}\n",
+ now_ms(), source, unit.unwrap_or("-"), ente_cas::hex(&sha)
+ );
+ let path = index_path();
+ let _guard = INDEX_FILE.lock().unwrap();
+ if let Some(parent) = path.parent() {
+ let _ = std::fs::create_dir_all(parent);
+ }
+ use std::io::Write;
+ let mut f = match std::fs::OpenOptions::new()
+ .create(true).append(true)
+ .open(&path)
+ {
+ Ok(f) => f,
+ Err(e) => { warn!(?e, path = %path.display(), "abrir index"); return; }
+ };
+ if let Err(e) = f.write_all(line.as_bytes()) {
+ warn!(?e, "write index");
+ }
+}
+
/// Decodifica best-effort. Formato journald nativo: lines de "KEY=value"
/// (binario para values con newlines, pero raro). Formato syslog: texto
/// con prefijo "<priority>tag: message".
fn handle_message(buf: &[u8], source: &'static str) {
if let Ok(s) = std::str::from_utf8(buf) {
- // Heurística: si tiene '=' en alguna línea, asumir journald.
if s.contains('=') && s.lines().any(|l| l.contains('=')) {
let mut message = None;
let mut priority = None;
- let mut unit = None;
+ let mut unit: Option<String> = None;
for line in s.lines() {
if let Some((k, v)) = line.split_once('=') {
match k {
@@ -122,16 +172,18 @@ fn handle_message(buf: &[u8], source: &'static str) {
}
}
}
+ persist_to_cas(buf, source, unit.as_deref());
if let Some(msg) = message {
info!(target: "journal", source, ?priority, ?unit, "{msg}");
} else {
debug!(source, len = buf.len(), "journal native sin MESSAGE");
}
} else {
- // Syslog
+ persist_to_cas(buf, source, None);
info!(target: "syslog", source, "{}", s.trim_end());
}
} else {
+ persist_to_cas(buf, source, None);
debug!(source, len = buf.len(), "journal binario (no UTF-8)");
}
}
diff --git a/crates/ente-localed-compat/src/main.rs b/crates/ente-localed-compat/src/main.rs
index 24e3241..f553c54 100644
--- a/crates/ente-localed-compat/src/main.rs
+++ b/crates/ente-localed-compat/src/main.rs
@@ -96,8 +96,21 @@ impl LocaleManager {
}
async fn set_locale(&self, locale: Vec<String>, _interactive: bool) -> fdo::Result<()> {
- info!(?locale, "SetLocale (stub: no persistimos a /etc/locale.conf)");
- *self.transient_locale.lock().unwrap() = Some(locale);
+ // Validar formato KEY=value en cada entry.
+ for entry in &locale {
+ if !entry.contains('=') {
+ return Err(fdo::Error::InvalidArgs(
+ format!("locale entry inválido (sin '='): {entry}")
+ ));
+ }
+ }
+ let content: String = locale.iter()
+ .map(|s| format!("{s}\n"))
+ .collect();
+ atomic_write("/etc/locale.conf", content.as_bytes())
+ .map_err(|e| fdo::Error::Failed(format!("write /etc/locale.conf: {e}")))?;
+ *self.transient_locale.lock().unwrap() = Some(locale.clone());
+ info!(?locale, "SetLocale → /etc/locale.conf");
Ok(())
}
@@ -126,6 +139,24 @@ impl LocaleManager {
}
}
+fn atomic_write(path: &str, content: &[u8]) -> std::io::Result<()> {
+ use std::io::Write;
+ use std::os::unix::fs::OpenOptionsExt;
+ let p = std::path::Path::new(path);
+ if let Some(parent) = p.parent() { let _ = std::fs::create_dir_all(parent); }
+ let tmp = p.with_extension("tmp");
+ {
+ let mut f = std::fs::OpenOptions::new()
+ .create(true).write(true).truncate(true)
+ .mode(0o644)
+ .open(&tmp)?;
+ f.write_all(content)?;
+ f.sync_all()?;
+ }
+ std::fs::rename(&tmp, p)?;
+ Ok(())
+}
+
fn read_kv(path: &str, key: &str) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
for line in content.lines() {
diff --git a/crates/ente-polkit-compat/Cargo.toml b/crates/ente-polkit-compat/Cargo.toml
new file mode 100644
index 0000000..9aa81f5
--- /dev/null
+++ b/crates/ente-polkit-compat/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "ente-polkit-compat"
+version = "0.0.1"
+edition.workspace = true
+license.workspace = true
+publish.workspace = true
+
+[[bin]]
+name = "ente-polkit-compat"
+path = "src/main.rs"
+
+[dependencies]
+ente-card = { path = "../ente-card" }
+ente-bus = { path = "../ente-bus" }
+anyhow = { workspace = true }
+tokio = { workspace = true }
+tracing = { workspace = true }
+tracing-subscriber = { workspace = true }
+zbus = { version = "4", default-features = false, features = ["tokio"] }
diff --git a/crates/ente-polkit-compat/src/main.rs b/crates/ente-polkit-compat/src/main.rs
new file mode 100644
index 0000000..96fd8c1
--- /dev/null
+++ b/crates/ente-polkit-compat/src/main.rs
@@ -0,0 +1,208 @@
+//! ente-polkit-compat: shim de `org.freedesktop.PolicyKit1.Authority`.
+//!
+//! Polkit autoriza llamadas privilegiadas (e.g. SetHostname, PowerOff).
+//! En el fractal no usamos polkit como gatekeeper — la auth se hace en
+//! el bus interno via SO_PEERCRED y capability grants. Pero apps que
+//! usan polkit (gnome-control-center, etc) bloquean en `CheckAuthorization`
+//! si no responde nadie.
+//!
+//! Este shim responde "is_authorized=true" siempre — el fractal queda
+//! como sistema confiado. El logging deja audit trail de qué acciones se
+//! han pedido para futuro análisis.
+//!
+//! Producción real: integrar con el grant system del bus interno —
+//! CheckAuthorization solicita un token al graph y devuelve true/false
+//! según el resultado.
+
+use ente_bus::{BusClient, BusRequest, BusResponse};
+use ente_card::Capability;
+use std::collections::HashMap;
+use tokio::signal::unix::{signal, SignalKind};
+use tracing::{info, warn};
+use tracing_subscriber::EnvFilter;
+use zbus::{fdo, interface, zvariant::OwnedValue};
+
+const BUS_NAME: &str = "org.freedesktop.PolicyKit1";
+const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/Authority";
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> anyhow::Result<()> {
+ init_tracing();
+ info!("ente-polkit-compat: arrancando");
+ announce_to_fractal().await;
+
+ let manager = PolkitAuthority;
+ let conn_result = zbus::connection::Builder::system()
+ .and_then(|b| b.name(BUS_NAME))
+ .and_then(|b| b.serve_at(OBJ_PATH, manager));
+ match conn_result {
+ Ok(builder) => match builder.build().await {
+ Ok(_conn) => {
+ info!(name = BUS_NAME, "name acquired, sirviendo");
+ wait_for_term().await
+ }
+ Err(e) => { warn!(?e, "build conn falló — modo idle"); wait_for_term().await }
+ },
+ Err(e) => { warn!(?e, "builder D-Bus falló — modo idle"); wait_for_term().await }
+ }
+}
+
+struct PolkitAuthority;
+
+/// Wire format de Polkit: `Subject = (s, a{sv})` — kind ("unix-session",
+/// "unix-process", "system-bus-name") + detalles. El detail típico:
+/// {"pid": u32, "start-time": u64, "uid": u32}
+type Subject = (String, HashMap<String, OwnedValue>);
+
+/// Resultado de `CheckAuthorization`: `(b, b, a{ss})` —
+/// is_authorized, is_challenge, details.
+type AuthResult = (bool, bool, HashMap<String, String>);
+
+#[interface(name = "org.freedesktop.PolicyKit1.Authority")]
+impl PolkitAuthority {
+ async fn check_authorization(
+ &self,
+ subject: Subject,
+ action_id: String,
+ _details: HashMap<String, String>,
+ _flags: u32,
+ _cancellation_id: String,
+ ) -> fdo::Result<AuthResult> {
+ let (subj_kind, subj_details) = subject;
+ let pid = subj_details.get("pid")
+ .and_then(|v| u32::try_from(v).ok());
+ let uid = subj_details.get("uid")
+ .and_then(|v| u32::try_from(v).ok());
+ info!(%action_id, %subj_kind, ?pid, ?uid, "CheckAuthorization → ALLOW");
+ // Always-yes: fractal confía en todos sus Entes (auth real está en el bus).
+ Ok((true, false, HashMap::new()))
+ }
+
+ async fn check_authorization_by_async(
+ &self,
+ subject: Subject,
+ action_id: String,
+ details: HashMap<String, String>,
+ flags: u32,
+ cancellation_id: String,
+ ) -> fdo::Result<AuthResult> {
+ // Mismo comportamiento; algunos clientes llaman la versión async.
+ self.check_authorization(subject, action_id, details, flags, cancellation_id).await
+ }
+
+ async fn cancel_check_authorization(&self, _cancellation_id: String) -> fdo::Result<()> {
+ Ok(())
+ }
+
+ async fn enumerate_actions(&self, _locale: String) -> fdo::Result<Vec<EnumeratedAction>> {
+ // Devolvemos lista vacía — no enumeramos acciones registradas.
+ // El llamador (típicamente gnome-control-center settings panel)
+ // debería degradar grácilmente.
+ Ok(vec![])
+ }
+
+ async fn register_authentication_agent(
+ &self,
+ _subject: Subject,
+ _locale: String,
+ _object_path: String,
+ ) -> fdo::Result<()> {
+ info!("RegisterAuthenticationAgent (no-op)");
+ Ok(())
+ }
+
+ async fn register_authentication_agent_with_options(
+ &self,
+ _subject: Subject,
+ _locale: String,
+ _object_path: String,
+ _options: HashMap<String, OwnedValue>,
+ ) -> fdo::Result<()> {
+ Ok(())
+ }
+
+ async fn unregister_authentication_agent(
+ &self,
+ _subject: Subject,
+ _object_path: String,
+ ) -> fdo::Result<()> {
+ Ok(())
+ }
+
+ async fn authentication_agent_response(
+ &self,
+ _cookie: String,
+ _identity: (String, HashMap<String, OwnedValue>),
+ ) -> fdo::Result<()> {
+ Ok(())
+ }
+
+ async fn enumerate_temporary_authorizations(
+ &self,
+ _subject: Subject,
+ ) -> fdo::Result<Vec<TemporaryAuth>> {
+ Ok(vec![])
+ }
+
+ async fn revoke_temporary_authorizations(&self, _subject: Subject) -> fdo::Result<()> {
+ Ok(())
+ }
+
+ async fn revoke_temporary_authorization_by_id(&self, _id: String) -> fdo::Result<()> {
+ Ok(())
+ }
+
+ #[zbus(property)]
+ async fn backend_name(&self) -> String { "ente-polkit-compat".into() }
+
+ #[zbus(property)]
+ async fn backend_version(&self) -> String { env!("CARGO_PKG_VERSION").into() }
+
+ #[zbus(property)]
+ async fn backend_features(&self) -> u32 { 0 }
+}
+
+/// Wire signature de EnumerateActions item:
+/// `(ssssssuusa{ss})` — action_id, descripción, message, vendor, vendor_url,
+/// icon_name, implicit_any, implicit_inactive, implicit_active, annotations.
+type EnumeratedAction = (
+ String, String, String, String, String, String,
+ u32, u32, String, HashMap<String, String>,
+);
+
+/// Wire signature de TemporaryAuthorization:
+/// `(sssss)` — id, action_id, subject_kind, subject_detail, time_obtained, time_expires.
+/// Aquí `(string)` * 5 + 2 timestamps. Simplificamos al subset relevante.
+type TemporaryAuth = (String, String, (String, HashMap<String, OwnedValue>), u64, u64);
+
+async fn announce_to_fractal() {
+ if let Ok(mut client) = BusClient::from_env().await {
+ let req = BusRequest::Announce {
+ capabilities: vec![Capability::Endpoint {
+ interface: ente_card::InterfaceId([0xa4; 16]),
+ version: 1,
+ }],
+ };
+ match client.call(req).await {
+ Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
+ Ok(other) => warn!(?other, "Announce respuesta inesperada"),
+ Err(e) => warn!(?e, "Announce falló"),
+ }
+ }
+}
+
+async fn wait_for_term() -> anyhow::Result<()> {
+ let mut term = signal(SignalKind::terminate())?;
+ let mut int_ = signal(SignalKind::interrupt())?;
+ tokio::select! {
+ _ = term.recv() => info!("SIGTERM"),
+ _ = int_.recv() => info!("SIGINT"),
+ }
+ Ok(())
+}
+
+fn init_tracing() {
+ let filter = EnvFilter::try_from_default_env()
+ .unwrap_or_else(|_| EnvFilter::new("ente_polkit_compat=info"));
+ tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
+}
diff --git a/crates/ente-resolved-compat/Cargo.toml b/crates/ente-resolved-compat/Cargo.toml
new file mode 100644
index 0000000..4a351de
--- /dev/null
+++ b/crates/ente-resolved-compat/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "ente-resolved-compat"
+version = "0.0.1"
+edition.workspace = true
+license.workspace = true
+publish.workspace = true
+
+[[bin]]
+name = "ente-resolved-compat"
+path = "src/main.rs"
+
+[dependencies]
+ente-card = { path = "../ente-card" }
+ente-bus = { path = "../ente-bus" }
+libc = { workspace = true }
+anyhow = { workspace = true }
+tokio = { workspace = true }
+tracing = { workspace = true }
+tracing-subscriber = { workspace = true }
+zbus = { version = "4", default-features = false, features = ["tokio"] }
diff --git a/crates/ente-resolved-compat/src/main.rs b/crates/ente-resolved-compat/src/main.rs
new file mode 100644
index 0000000..a103bbe
--- /dev/null
+++ b/crates/ente-resolved-compat/src/main.rs
@@ -0,0 +1,210 @@
+//! ente-resolved-compat: shim de `org.freedesktop.resolve1`.
+//!
+//! Bajo el capó usa `tokio::net::lookup_host` (que termina en getaddrinfo
+//! del libc del sistema). No reimplementamos un resolver DNS — delegamos
+//! al stack de resolución del kernel/glibc.
+//!
+//! Métodos cubiertos:
+//! - ResolveHostname (name → addresses)
+//! - ResolveAddress (address → name reverse)
+//! - ResolveRecord (TXT/SRV/etc) — NotSupported (requiere DNS query directa)
+
+use ente_bus::{BusClient, BusRequest, BusResponse};
+use ente_card::Capability;
+use std::net::IpAddr;
+use tokio::signal::unix::{signal, SignalKind};
+use tracing::{info, warn};
+use tracing_subscriber::EnvFilter;
+use zbus::{fdo, interface};
+
+const BUS_NAME: &str = "org.freedesktop.resolve1";
+const OBJ_PATH: &str = "/org/freedesktop/resolve1";
+
+const AF_INET: i32 = 2;
+const AF_INET6: i32 = 10;
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> anyhow::Result<()> {
+ init_tracing();
+ info!("ente-resolved-compat: arrancando");
+ announce_to_fractal().await;
+
+ let manager = ResolveManager;
+ let conn_result = zbus::connection::Builder::system()
+ .and_then(|b| b.name(BUS_NAME))
+ .and_then(|b| b.serve_at(OBJ_PATH, manager));
+ match conn_result {
+ Ok(builder) => match builder.build().await {
+ Ok(_conn) => {
+ info!(name = BUS_NAME, "name acquired, sirviendo");
+ wait_for_term().await
+ }
+ Err(e) => { warn!(?e, "build conn falló — modo idle"); wait_for_term().await }
+ },
+ Err(e) => { warn!(?e, "builder D-Bus falló — modo idle"); wait_for_term().await }
+ }
+}
+
+struct ResolveManager;
+
+/// Tipo del wire format de `ResolveHostname`. Por entry: (ifindex, family,
+/// address-as-bytes). systemd-resolved devuelve hasta 4 bytes para AF_INET
+/// y 16 para AF_INET6.
+type HostnameAddress = (i32, i32, Vec<u8>);
+
+#[interface(name = "org.freedesktop.resolve1.Manager")]
+impl ResolveManager {
+ /// Wire signature: `ResolveHostname(in iiusst, out a(iiay)st)` — recibe
+ /// (ifindex, name, family, flags), devuelve (addresses, canonical, flags).
+ async fn resolve_hostname(
+ &self,
+ _ifindex: i32,
+ name: String,
+ family: i32,
+ _flags: u64,
+ ) -> fdo::Result<(Vec<HostnameAddress>, String, u64)> {
+ // tokio::net::lookup_host requiere "host:port"; usamos puerto sentinel.
+ let target = format!("{name}:0");
+ let addrs = match tokio::net::lookup_host(&target).await {
+ Ok(it) => it,
+ Err(e) => return Err(fdo::Error::Failed(format!("lookup_host {name}: {e}"))),
+ };
+ let mut out = Vec::new();
+ for sa in addrs {
+ let ip = sa.ip();
+ let (af, bytes) = match ip {
+ IpAddr::V4(v4) => (AF_INET, v4.octets().to_vec()),
+ IpAddr::V6(v6) => (AF_INET6, v6.octets().to_vec()),
+ };
+ // Filtrado por family si el llamador lo pidió específico.
+ if family != 0 && family != af { continue; }
+ out.push((0i32, af, bytes));
+ }
+ if out.is_empty() {
+ return Err(fdo::Error::Failed(format!("sin resoluciones para {name} (family={family})")));
+ }
+ info!(%name, family, count = out.len(), "ResolveHostname");
+ Ok((out, name, 0))
+ }
+
+ /// Wire signature: `ResolveAddress(in iiayt, out a(is)t)` — (ifindex,
+ /// family, address, flags) → (names, flags).
+ async fn resolve_address(
+ &self,
+ _ifindex: i32,
+ family: i32,
+ address: Vec<u8>,
+ _flags: u64,
+ ) -> fdo::Result<(Vec<(i32, String)>, u64)> {
+ let ip = parse_address(family, &address)
+ .ok_or_else(|| fdo::Error::InvalidArgs(format!("address malformado family={family} bytes={}", address.len())))?;
+ // Reverse lookup vía getnameinfo. Usamos std::net::lookup_addr no existe,
+ // así que invocamos via libc directamente.
+ let name = reverse_lookup(ip)
+ .ok_or_else(|| fdo::Error::Failed(format!("sin reverse para {ip}")))?;
+ info!(%ip, %name, "ResolveAddress");
+ Ok((vec![(0, name)], 0))
+ }
+
+ async fn resolve_record(
+ &self,
+ _ifindex: i32,
+ _name: String,
+ _class: u16,
+ _type_: u16,
+ _flags: u64,
+ ) -> fdo::Result<(Vec<(i32, u16, u16, Vec<u8>)>, u64)> {
+ Err(fdo::Error::NotSupported(
+ "ResolveRecord requiere acceso DNS directo — stub no implementado".into()
+ ))
+ }
+}
+
+fn parse_address(family: i32, bytes: &[u8]) -> Option<IpAddr> {
+ match family {
+ AF_INET if bytes.len() == 4 => {
+ let mut a = [0u8; 4];
+ a.copy_from_slice(bytes);
+ Some(IpAddr::V4(std::net::Ipv4Addr::from(a)))
+ }
+ AF_INET6 if bytes.len() == 16 => {
+ let mut a = [0u8; 16];
+ a.copy_from_slice(bytes);
+ Some(IpAddr::V6(std::net::Ipv6Addr::from(a)))
+ }
+ _ => None,
+ }
+}
+
+/// getnameinfo(3) wrapper. Devuelve None si no resuelve.
+fn reverse_lookup(ip: IpAddr) -> Option<String> {
+ use std::os::raw::c_char;
+ let mut buf = [0i8; 256];
+ let r = match ip {
+ IpAddr::V4(v4) => unsafe {
+ let octets = v4.octets();
+ let mut sin = std::mem::zeroed::<libc::sockaddr_in>();
+ sin.sin_family = libc::AF_INET as u16;
+ sin.sin_addr = libc::in_addr {
+ s_addr: u32::from_ne_bytes(octets),
+ };
+ libc::getnameinfo(
+ &sin as *const _ as *const libc::sockaddr,
+ std::mem::size_of::<libc::sockaddr_in>() as u32,
+ buf.as_mut_ptr() as *mut c_char, buf.len() as u32,
+ std::ptr::null_mut(), 0,
+ libc::NI_NAMEREQD,
+ )
+ },
+ IpAddr::V6(v6) => unsafe {
+ let octets = v6.octets();
+ let mut sin6 = std::mem::zeroed::<libc::sockaddr_in6>();
+ sin6.sin6_family = libc::AF_INET6 as u16;
+ sin6.sin6_addr.s6_addr.copy_from_slice(&octets);
+ libc::getnameinfo(
+ &sin6 as *const _ as *const libc::sockaddr,
+ std::mem::size_of::<libc::sockaddr_in6>() as u32,
+ buf.as_mut_ptr() as *mut c_char, buf.len() as u32,
+ std::ptr::null_mut(), 0,
+ libc::NI_NAMEREQD,
+ )
+ },
+ };
+ if r != 0 { return None; }
+ let cs = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
+ cs.to_str().ok().map(String::from)
+}
+
+extern crate libc;
+
+async fn announce_to_fractal() {
+ if let Ok(mut client) = BusClient::from_env().await {
+ let req = BusRequest::Announce {
+ capabilities: vec![Capability::Endpoint {
+ interface: ente_card::InterfaceId([0xa3; 16]),
+ version: 1,
+ }],
+ };
+ match client.call(req).await {
+ Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
+ Ok(other) => warn!(?other, "Announce respuesta inesperada"),
+ Err(e) => warn!(?e, "Announce falló"),
+ }
+ }
+}
+
+async fn wait_for_term() -> anyhow::Result<()> {
+ let mut term = signal(SignalKind::terminate())?;
+ let mut int_ = signal(SignalKind::interrupt())?;
+ tokio::select! {
+ _ = term.recv() => info!("SIGTERM"),
+ _ = int_.recv() => info!("SIGINT"),
+ }
+ Ok(())
+}
+
+fn init_tracing() {
+ let filter = EnvFilter::try_from_default_env()
+ .unwrap_or_else(|_| EnvFilter::new("ente_resolved_compat=info"));
+ tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
+}
diff --git a/crates/ente-timedated-compat/src/main.rs b/crates/ente-timedated-compat/src/main.rs
index 1ad2b0b..a224c41 100644
--- a/crates/ente-timedated-compat/src/main.rs
+++ b/crates/ente-timedated-compat/src/main.rs
@@ -105,7 +105,21 @@ impl TimedateManager {
}
async fn set_timezone(&self, timezone: String, _interactive: bool) -> fdo::Result<()> {
- info!(%timezone, "SetTimezone (stub: no actualizamos /etc/localtime)");
+ // Validar contra zoneinfo: el archivo destino debe existir.
+ let zoneinfo = format!("/usr/share/zoneinfo/{timezone}");
+ if !std::path::Path::new(&zoneinfo).exists() {
+ return Err(fdo::Error::InvalidArgs(format!("timezone desconocida: {timezone}")));
+ }
+ // Atomic relink: crear localtime.tmp como symlink, rename.
+ let tmp = "/etc/localtime.tmp";
+ let _ = std::fs::remove_file(tmp);
+ if let Err(e) = std::os::unix::fs::symlink(&zoneinfo, tmp) {
+ return Err(fdo::Error::Failed(format!("symlink: {e}")));
+ }
+ if let Err(e) = std::fs::rename(tmp, "/etc/localtime") {
+ return Err(fdo::Error::Failed(format!("rename: {e}")));
+ }
+ info!(%timezone, "SetTimezone → /etc/localtime");
Ok(())
}
diff --git a/crates/ente-zero/src/seed.rs b/crates/ente-zero/src/seed.rs
index 9dfada5..06365ab 100644
--- a/crates/ente-zero/src/seed.rs
+++ b/crates/ente-zero/src/seed.rs
@@ -153,6 +153,8 @@ fn synthesize_dev_seed() -> EntityCard {
("compat-timedated", "target/debug/ente-timedated-compat"),
("compat-localed", "target/debug/ente-localed-compat"),
("compat-journald", "target/debug/ente-journald-compat"),
+ ("compat-resolved", "target/debug/ente-resolved-compat"),
+ ("compat-polkit", "target/debug/ente-polkit-compat"),
] {
if let Some(card) = optional_native_card(
label, bin,