mirror of
https://github.com/safing/portmaster
synced 2025-04-08 21:19:10 +00:00
246 lines
8.2 KiB
Rust
246 lines
8.2 KiB
Rust
use log::{debug, error};
|
|
|
|
use super::status::StatusResult;
|
|
use super::{Result, ServiceManager, ServiceManagerError};
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::{
|
|
fs, io,
|
|
process::{Command, ExitStatus, Stdio},
|
|
};
|
|
|
|
static SYSTEMCTL: &str = "systemctl";
|
|
// TODO(ppacher): add support for kdesudo and gksudo
|
|
|
|
enum SudoCommand {
|
|
Pkexec,
|
|
Gksu,
|
|
}
|
|
|
|
impl From<std::process::Output> for ServiceManagerError {
|
|
fn from(output: std::process::Output) -> Self {
|
|
let msg = String::from_utf8(output.stderr)
|
|
.ok()
|
|
.filter(|s| !s.trim().is_empty())
|
|
.or_else(|| {
|
|
String::from_utf8(output.stdout)
|
|
.ok()
|
|
.filter(|s| !s.trim().is_empty())
|
|
})
|
|
.unwrap_or_else(|| format!("Failed to run `systemctl`"));
|
|
|
|
ServiceManagerError::Other(output.status, msg)
|
|
}
|
|
}
|
|
|
|
/// System Service manager implementation for Linux based distros.
|
|
pub struct SystemdServiceManager {}
|
|
|
|
impl SystemdServiceManager {
|
|
/// Checks if systemctl is available in /sbin/ /bin, /usr/bin or /usr/sbin.
|
|
///
|
|
/// Note that we explicitly check those paths to avoid returning true in case
|
|
/// there's a systemctl binary in the cwd and PATH includes . since this may
|
|
/// pose a security risk of running an untrusted binary with root privileges.
|
|
pub fn is_installed() -> bool {
|
|
let paths = vec![
|
|
"/sbin/systemctl",
|
|
"/bin/systemctl",
|
|
"/usr/sbin/systemctl",
|
|
"/usr/bin/systemctl",
|
|
];
|
|
|
|
for path in paths {
|
|
debug!("checking for systemctl at path {}", path);
|
|
|
|
match fs::metadata(path) {
|
|
Ok(md) => {
|
|
debug!("found systemctl at path {} ", path);
|
|
|
|
if md.is_file() && md.permissions().mode() & 0o111 != 0 {
|
|
return true;
|
|
}
|
|
|
|
error!(
|
|
"systemctl binary found but invalid permissions: {}",
|
|
md.permissions().mode().to_string()
|
|
);
|
|
}
|
|
Err(err) => {
|
|
error!(
|
|
"failed to check systemctl binary at {}: {}",
|
|
path,
|
|
err.to_string()
|
|
);
|
|
|
|
continue;
|
|
}
|
|
};
|
|
}
|
|
|
|
error!("failed to find systemctl binary");
|
|
|
|
false
|
|
}
|
|
}
|
|
|
|
impl ServiceManager for SystemdServiceManager {
|
|
fn status(&self) -> super::Result<StatusResult> {
|
|
let name = "portmaster.service";
|
|
let result = systemctl("is-active", name, false);
|
|
|
|
match result {
|
|
// If `systemctl is-active` returns without an error code and stdout matches "active" (just to guard againt
|
|
// unhandled cases), the service can be considered running.
|
|
Ok(stdout) => {
|
|
let mut copy = stdout.to_owned();
|
|
trim_newline(&mut copy);
|
|
|
|
if copy != "active" {
|
|
// make sure the output is as we expected
|
|
Err(ServiceManagerError::Other(ExitStatus::default(), stdout))
|
|
} else {
|
|
Ok(StatusResult::Running)
|
|
}
|
|
}
|
|
|
|
Err(e) => {
|
|
if let ServiceManagerError::Other(_err, ref output) = e {
|
|
let mut copy = output.to_owned();
|
|
trim_newline(&mut copy);
|
|
|
|
if copy == "inactive" {
|
|
return Ok(StatusResult::Stopped);
|
|
}
|
|
} else {
|
|
error!("failed to run 'systemctl is-active': {}", e.to_string());
|
|
}
|
|
|
|
// Failed to check if the unit is running
|
|
match systemctl("cat", name, false) {
|
|
// "systemctl cat" seems to no have stable exit codes so we need
|
|
// to check the output if it looks like "No files found for yyyy.service"
|
|
// At least, the exit code are not documented for systemd v255 (newest at the time of writing)
|
|
Err(ServiceManagerError::Other(status, msg)) => {
|
|
if msg.contains("No files found for") {
|
|
Ok(StatusResult::NotFound)
|
|
} else {
|
|
Err(ServiceManagerError::Other(status, msg))
|
|
}
|
|
}
|
|
|
|
// Any other error type means something went completely wrong while running systemctl altogether.
|
|
Err(e) => Err(e),
|
|
|
|
// Fine, systemctl cat worked so if the output is "inactive" we know the service is installed
|
|
// but stopped.
|
|
Ok(_) => {
|
|
// Unit seems to be installed so check the output of result
|
|
let mut stderr = e.to_string();
|
|
trim_newline(&mut stderr);
|
|
|
|
if stderr == "inactive" {
|
|
Ok(StatusResult::Stopped)
|
|
} else {
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn start(&self) -> Result<StatusResult> {
|
|
let name = "portmaster.service";
|
|
|
|
// This time we need to run as root through pkexec or similar binaries like kdesudo/gksudo.
|
|
systemctl("start", name, true)?;
|
|
|
|
// Check the status again to be sure it's started now
|
|
self.status()
|
|
}
|
|
}
|
|
|
|
fn systemctl(
|
|
cmd: &str,
|
|
unit: &str,
|
|
run_as_root: bool,
|
|
) -> std::result::Result<String, ServiceManagerError> {
|
|
let output = run(run_as_root, SYSTEMCTL, vec![cmd, unit])?;
|
|
|
|
// The command have been able to run (i.e. has been spawned and executed by the kernel).
|
|
// We now need to check the exit code and "stdout/stderr" output in case of an error.
|
|
if output.status.success() {
|
|
Ok(String::from_utf8(output.stdout)?)
|
|
} else {
|
|
Err(output.into())
|
|
}
|
|
}
|
|
|
|
fn run<'a>(root: bool, cmd: &'a str, args: Vec<&'a str>) -> std::io::Result<std::process::Output> {
|
|
// clone the args vector so we can insert the actual command in case we're running
|
|
// through pkexec or friends. This is just callled a couple of times on start-up
|
|
// so cloning the vector does not add any mentionable performance impact here and it's better
|
|
// than expecting a mutalble vector in the first place.
|
|
|
|
let mut args = args.to_vec();
|
|
|
|
let mut command = match root {
|
|
true => {
|
|
// if we run through pkexec and friends we need to append cmd as the second argument.
|
|
|
|
args.insert(0, cmd);
|
|
match get_sudo_cmd() {
|
|
Ok(cmd) => {
|
|
match cmd {
|
|
SudoCommand::Pkexec => {
|
|
// disable the internal text-based prompt agent from pkexec because it won't work anyway.
|
|
args.insert(0, "--disable-internal-agent");
|
|
Command::new("/usr/bin/pkexec")
|
|
}
|
|
SudoCommand::Gksu => {
|
|
args.insert(0, "--message=Please enter your password:");
|
|
args.insert(1, "--sudo-mode");
|
|
|
|
Command::new("/usr/bin/gksudo")
|
|
}
|
|
}
|
|
}
|
|
Err(err) => return Err(err),
|
|
}
|
|
}
|
|
false => Command::new(cmd),
|
|
};
|
|
|
|
command.env("LC_ALL", "C");
|
|
|
|
command
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
command.args(args).output()
|
|
}
|
|
|
|
fn trim_newline(s: &mut String) {
|
|
if s.ends_with('\n') {
|
|
s.pop();
|
|
if s.ends_with('\r') {
|
|
s.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_sudo_cmd() -> std::result::Result<SudoCommand, std::io::Error> {
|
|
if let Ok(_) = fs::metadata("/usr/bin/pkexec") {
|
|
return Ok(SudoCommand::Pkexec);
|
|
}
|
|
|
|
if let Ok(_) = fs::metadata("/usr/bin/gksudo") {
|
|
return Ok(SudoCommand::Gksu);
|
|
}
|
|
|
|
Err(std::io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
"failed to detect sudo command",
|
|
))
|
|
}
|