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, 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: +/// `:::`. 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 "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 = 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, _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 { 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); + +/// Resultado de `CheckAuthorization`: `(b, b, a{ss})` — +/// is_authorized, is_challenge, details. +type AuthResult = (bool, bool, HashMap); + +#[interface(name = "org.freedesktop.PolicyKit1.Authority")] +impl PolkitAuthority { + async fn check_authorization( + &self, + subject: Subject, + action_id: String, + _details: HashMap, + _flags: u32, + _cancellation_id: String, + ) -> fdo::Result { + 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, + flags: u32, + cancellation_id: String, + ) -> fdo::Result { + // 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> { + // 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, + ) -> 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), + ) -> fdo::Result<()> { + Ok(()) + } + + async fn enumerate_temporary_authorizations( + &self, + _subject: Subject, + ) -> fdo::Result> { + 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, +); + +/// 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), 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); + +#[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, 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, + _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)>, u64)> { + Err(fdo::Error::NotSupported( + "ResolveRecord requiere acceso DNS directo — stub no implementado".into() + )) + } +} + +fn parse_address(family: i32, bytes: &[u8]) -> Option { + 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 { + 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::(); + 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::() 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::(); + 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::() 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,