use kftray_commons::models::config_model::Config;
use kftray_commons::utils::hostsfile::hosts_file_writable;
use serde::Serialize;
use crate::network_utils::{
is_address_accessible,
is_custom_loopback_address,
};
const APP_ID: &str = "com.kftray.app";
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PrivilegedResource {
HostsFile,
LoopbackAddress,
}
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub struct PrivilegeNeed {
pub config_id: i64,
pub resource: PrivilegedResource,
pub detail: String,
}
pub async fn preflight(configs: &[Config]) -> Vec<PrivilegeNeed> {
let probed = tokio::task::spawn_blocking(|| {
if helper_is_running() || process_is_root() {
return None;
}
Some(hosts_file_writable().unwrap_or(true))
})
.await
.unwrap_or(None);
let Some(hosts_writable) = probed else {
return Vec::new();
};
let mut needs = Vec::new();
for config in configs {
let Some(config_id) = config.id else {
continue;
};
for (resource, detail) in
needs_for(config, hosts_writable, loopback_aliases_need_privileges()).await
{
needs.push(PrivilegeNeed {
config_id,
resource,
detail,
});
}
}
needs
}
async fn needs_for(
config: &Config, hosts_writable: bool, aliases_need_privileges: bool,
) -> Vec<(PrivilegedResource, String)> {
let mut needs = Vec::new();
if config.workload_type.as_deref() == Some("expose") {
return needs;
}
let label = config
.alias
.clone()
.or_else(|| config.service.clone())
.unwrap_or_default();
if config.domain_enabled.unwrap_or_default() && config.service.is_some() && !hosts_writable {
needs.push((PrivilegedResource::HostsFile, label.clone()));
}
if config.auto_loopback_address {
if aliases_need_privileges {
needs.push((PrivilegedResource::LoopbackAddress, label));
}
} else if let Some(address) = config.local_address.as_deref()
&& is_custom_loopback_address(address)
&& !is_address_accessible(address).await
{
needs.push((PrivilegedResource::LoopbackAddress, address.to_string()));
}
needs
}
fn loopback_aliases_need_privileges() -> bool {
cfg!(target_os = "macos")
}
fn helper_is_running() -> bool {
use kftray_helper::client::socket_comm;
use kftray_helper::messages::{
RequestCommand,
RequestResult,
};
let Ok(socket_path) = kftray_helper::communication::get_default_socket_path() else {
return false;
};
if !socket_comm::is_socket_available(&socket_path) {
return false;
}
matches!(
socket_comm::send_request(&socket_path, APP_ID, RequestCommand::Ping),
Ok(response) if matches!(&response.result, RequestResult::StringSuccess(s) if s == "pong")
)
}
fn process_is_root() -> bool {
#[cfg(target_os = "linux")]
{
unsafe { libc::geteuid() == 0 }
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn config() -> Config {
Config {
id: Some(1),
service: Some("svc".to_string()),
alias: Some("svc.local".to_string()),
namespace: "ns".to_string(),
protocol: "tcp".to_string(),
workload_type: Some("service".to_string()),
..Default::default()
}
}
#[tokio::test]
async fn a_plain_forward_needs_nothing() {
assert!(needs_for(&config(), false, true).await.is_empty());
}
#[tokio::test]
async fn a_domain_alias_needs_the_hosts_file_only_when_it_is_not_writable() {
let mut cfg = config();
cfg.domain_enabled = Some(true);
assert_eq!(
needs_for(&cfg, false, false).await,
vec![(PrivilegedResource::HostsFile, "svc.local".to_string())]
);
assert!(needs_for(&cfg, true, false).await.is_empty());
}
#[tokio::test]
async fn an_auto_allocated_address_needs_an_alias_only_where_aliases_are_privileged() {
let mut cfg = config();
cfg.auto_loopback_address = true;
assert_eq!(
needs_for(&cfg, true, true).await,
vec![(PrivilegedResource::LoopbackAddress, "svc.local".to_string())]
);
assert!(needs_for(&cfg, true, false).await.is_empty());
}
#[tokio::test]
async fn a_custom_address_that_already_binds_needs_nothing() {
let mut cfg = config();
cfg.local_address = Some("127.0.0.1".to_string());
assert!(needs_for(&cfg, true, true).await.is_empty());
}
#[tokio::test]
async fn an_expose_configuration_needs_nothing_locally() {
let mut cfg = config();
cfg.workload_type = Some("expose".to_string());
cfg.domain_enabled = Some(true);
cfg.auto_loopback_address = true;
assert!(needs_for(&cfg, false, true).await.is_empty());
}
}