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>
This commit is contained in:
Sergio
2026-05-04 09:53:42 +00:00
parent 6ad6d08fa8
commit d88a9c5791
12 changed files with 683 additions and 15 deletions
Generated
+28
View File
@@ -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"
+2
View File
@@ -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]
+89 -8
View File
@@ -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 {
+1
View File
@@ -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 }
+56 -4
View File
@@ -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)");
}
}
+33 -2
View File
@@ -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() {
+19
View File
@@ -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"] }
+208
View File
@@ -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();
}
+20
View File
@@ -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"] }
+210
View File
@@ -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();
}
+15 -1
View File
@@ -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(())
}
+2
View File
@@ -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,