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:
Generated
+28
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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"] }
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user