mirror of
https://github.com/telemt/telemt.git
synced 2026-10-31 21:02:01 +03:00
Fix FreeBSD service runtime ownership and privilege drop #913
This commit is contained in:
+55
-2
@@ -97,6 +97,9 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let init_system = service::detect_init_system();
|
let init_system = service::detect_init_system();
|
||||||
eprintln!("[+] Detected init system: {}", init_system);
|
eprintln!("[+] Detected init system: {}", init_system);
|
||||||
|
let service_path = service::service_file_path(init_system);
|
||||||
|
// A port-managed rc script and its configuration must survive --init.
|
||||||
|
ensure_service_install_allowed(init_system, Path::new(service_path))?;
|
||||||
|
|
||||||
let secret = match opts.secret {
|
let secret = match opts.secret {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
@@ -124,7 +127,7 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let service_opts = ServiceOptions {
|
let service_opts = ServiceOptions {
|
||||||
exe_path: &exe_path,
|
exe_path: &exe_path,
|
||||||
config_path: &config_path,
|
config_path: &config_path,
|
||||||
// Let the selected init system manage process identity.
|
// Preserve root startup unless an administrator configures a service identity.
|
||||||
user: None,
|
user: None,
|
||||||
group: None,
|
group: None,
|
||||||
pid_file: "/var/run/telemt.pid",
|
pid_file: "/var/run/telemt.pid",
|
||||||
@@ -132,7 +135,6 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
description: "Telemt MTProxy - Telegram MTProto Proxy",
|
description: "Telemt MTProxy - Telegram MTProto Proxy",
|
||||||
};
|
};
|
||||||
|
|
||||||
let service_path = service::service_file_path(init_system);
|
|
||||||
let service_content = service::generate_service_file(init_system, &service_opts);
|
let service_content = service::generate_service_file(init_system, &service_opts);
|
||||||
let service_mode = if init_system == InitSystem::OpenRC || init_system == InitSystem::FreeBSDRc
|
let service_mode = if init_system == InitSystem::OpenRC || init_system == InitSystem::FreeBSDRc
|
||||||
{
|
{
|
||||||
@@ -218,6 +220,26 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_service_install_allowed(
|
||||||
|
init_system: crate::service::InitSystem,
|
||||||
|
path: &Path,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
if init_system != crate::service::InitSystem::FreeBSDRc {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
match std::fs::symlink_metadata(path) {
|
||||||
|
Ok(_) => Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::AlreadyExists,
|
||||||
|
format!(
|
||||||
|
"refusing to replace existing FreeBSD service {}; manage it through its package or edit it explicitly",
|
||||||
|
path.display()
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn write_init_file(path: &Path, contents: &str, mode: u32) -> std::io::Result<()> {
|
fn write_init_file(path: &Path, contents: &str, mode: u32) -> std::io::Result<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
@@ -362,3 +384,34 @@ fn print_links(username: &str, secret: &str, port: u16, domain: &str) {
|
|||||||
println!("Check: journalctl -u telemt.service | head -30");
|
println!("Check: journalctl -u telemt.service | head -30");
|
||||||
println!("===================");
|
println!("===================");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::service::InitSystem;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freebsd_init_preserves_existing_service_files() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("telemt");
|
||||||
|
ensure_service_install_allowed(InitSystem::FreeBSDRc, &path).unwrap();
|
||||||
|
std::fs::write(&path, b"package-managed service").unwrap();
|
||||||
|
let error = ensure_service_install_allowed(InitSystem::FreeBSDRc, &path).unwrap_err();
|
||||||
|
assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
|
||||||
|
assert_eq!(std::fs::read(&path).unwrap(), b"package-managed service");
|
||||||
|
ensure_service_install_allowed(InitSystem::Systemd, &path).unwrap();
|
||||||
|
ensure_service_install_allowed(InitSystem::OpenRC, &path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn freebsd_init_rejects_dangling_service_symlinks() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("telemt");
|
||||||
|
let target = dir.path().join("missing");
|
||||||
|
std::os::unix::fs::symlink(&target, &path).unwrap();
|
||||||
|
let error = ensure_service_install_allowed(InitSystem::FreeBSDRc, &path).unwrap_err();
|
||||||
|
assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
|
||||||
|
assert_eq!(std::fs::read_link(&path).unwrap(), target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
use super::*;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
// Replace rc.subr and privileged commands, but execute the generated shell itself.
|
||||||
|
fn run_script(opts: &ServiceOptions, config: &str, fail_install: bool) -> std::process::Output {
|
||||||
|
let harness = format!(
|
||||||
|
r#"
|
||||||
|
load_rc_config() {{
|
||||||
|
:
|
||||||
|
{config}
|
||||||
|
}}
|
||||||
|
id() {{
|
||||||
|
printf '%s_primary\n' "$2"
|
||||||
|
}}
|
||||||
|
install() {{
|
||||||
|
printf 'install'
|
||||||
|
printf ' <%s>' "$@"
|
||||||
|
printf '\n'
|
||||||
|
{install_result}
|
||||||
|
}}
|
||||||
|
run_rc_command() {{
|
||||||
|
printf 'rc-user=<%s> rc-group=<%s>\n' "${{telemt_user-unset}}" "${{telemt_group-unset}}"
|
||||||
|
"$start_precmd" || return $?
|
||||||
|
printf 'pid=<%s>\n' "$pidfile"
|
||||||
|
eval "set -- $command_args"
|
||||||
|
printf 'arg=<%s>\n' "$@"
|
||||||
|
}}
|
||||||
|
"#,
|
||||||
|
install_result = if fail_install {
|
||||||
|
"return 19"
|
||||||
|
} else {
|
||||||
|
"return 0"
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let script = generate_freebsd_rc_script(opts).replace(". /etc/rc.subr", &harness);
|
||||||
|
let mut child = Command::new("/bin/sh")
|
||||||
|
.arg("-s")
|
||||||
|
.env_clear()
|
||||||
|
.env("PATH", "/usr/bin:/bin")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
child
|
||||||
|
.stdin
|
||||||
|
.take()
|
||||||
|
.unwrap()
|
||||||
|
.write_all(script.as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
child.wait_with_output().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freebsd_default_owns_only_service_directories_and_drops_privileges_internally() {
|
||||||
|
let output = run_script(&ServiceOptions::default(), "", false);
|
||||||
|
assert!(output.status.success(), "{:?}", output);
|
||||||
|
let text = String::from_utf8(output.stdout).unwrap();
|
||||||
|
let installs: Vec<_> = text
|
||||||
|
.lines()
|
||||||
|
.filter(|line| line.starts_with("install "))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
installs,
|
||||||
|
[
|
||||||
|
"install <-d> <-o> <telemt> <-g> <telemt> <-m> <755> </var/run/telemt>",
|
||||||
|
"install <-d> <-o> <telemt> <-g> <telemt> <-m> <755> </var/db/telemt>",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert!(text.contains("rc-user=<unset> rc-group=<unset>"));
|
||||||
|
assert!(text.contains("pid=</var/run/telemt/telemt.pid>"));
|
||||||
|
assert!(text.contains("arg=<--run-as-user>\narg=<telemt>"));
|
||||||
|
assert!(text.contains("arg=<--run-as-group>\narg=<telemt>"));
|
||||||
|
assert!(text.contains("arg=<--working-dir>\narg=</var/db/telemt>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freebsd_legacy_identity_settings_use_internal_privilege_drop() {
|
||||||
|
let output = run_script(
|
||||||
|
&ServiceOptions::default(),
|
||||||
|
"telemt_user=legacy\ntelemt_group=legacygroup",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let text = String::from_utf8(output.stdout).unwrap();
|
||||||
|
assert!(text.contains("rc-user=<unset> rc-group=<unset>"));
|
||||||
|
assert!(text.contains("arg=<--run-as-user>\narg=<legacy>"));
|
||||||
|
assert!(text.contains("arg=<--run-as-group>\narg=<legacygroup>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freebsd_new_identity_settings_override_legacy_aliases() {
|
||||||
|
let output = run_script(
|
||||||
|
&ServiceOptions::default(),
|
||||||
|
"telemt_user=legacy\ntelemt_group=legacygroup\ntelemt_uid=newuser\ntelemt_gid=newgroup",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let text = String::from_utf8(output.stdout).unwrap();
|
||||||
|
assert!(text.contains("arg=<--run-as-user>\narg=<newuser>"));
|
||||||
|
assert!(text.contains("arg=<--run-as-group>\narg=<newgroup>"));
|
||||||
|
assert!(!text.contains("legacy"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freebsd_root_start_preserves_privileged_network_initialization() {
|
||||||
|
let opts = ServiceOptions {
|
||||||
|
user: None,
|
||||||
|
group: None,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let output = run_script(&opts, "", false);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let text = String::from_utf8(output.stdout).unwrap();
|
||||||
|
assert!(!text.contains("arg=<--run-as-"));
|
||||||
|
assert!(text.contains("rc-user=<unset> rc-group=<unset>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freebsd_user_override_without_group_uses_the_accounts_primary_group() {
|
||||||
|
let opts = ServiceOptions {
|
||||||
|
user: None,
|
||||||
|
group: None,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for config in ["telemt_user=legacy", "telemt_uid=legacy"] {
|
||||||
|
let output = run_script(&opts, config, false);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let text = String::from_utf8(output.stdout).unwrap();
|
||||||
|
assert!(text.contains("arg=<--run-as-group>\narg=<legacy_primary>"));
|
||||||
|
assert!(text.contains("<-g> <legacy_primary>"));
|
||||||
|
assert!(!text.contains("wheel"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freebsd_custom_paths_are_preserved_without_chowning_the_pid_parent() {
|
||||||
|
let opts = ServiceOptions {
|
||||||
|
pid_file: "/custom run/proxy.pid",
|
||||||
|
working_dir: Some("/custom data/telemt"),
|
||||||
|
config_path: Path::new("/custom config/telemt.toml"),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let output = run_script(&opts, "", false);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let text = String::from_utf8(output.stdout).unwrap();
|
||||||
|
assert!(text.contains("pid=</custom run/proxy.pid>"));
|
||||||
|
assert!(text.contains("arg=</custom run/proxy.pid>"));
|
||||||
|
assert!(text.contains("arg=</custom data/telemt>"));
|
||||||
|
assert!(text.contains("arg=</custom config/telemt.toml>"));
|
||||||
|
assert!(
|
||||||
|
!text
|
||||||
|
.lines()
|
||||||
|
.any(|line| line.starts_with("install ") && line.ends_with("</custom run>"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freebsd_rc_conf_pid_override_does_not_change_shared_directory_ownership() {
|
||||||
|
let output = run_script(
|
||||||
|
&ServiceOptions::default(),
|
||||||
|
"telemt_pidfile=/var/run/custom.pid",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let text = String::from_utf8(output.stdout).unwrap();
|
||||||
|
assert!(text.contains("pid=</var/run/custom.pid>"));
|
||||||
|
assert!(
|
||||||
|
!text
|
||||||
|
.lines()
|
||||||
|
.any(|line| line.starts_with("install ") && line.ends_with("</var/run>"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freebsd_directory_setup_failure_aborts_startup() {
|
||||||
|
let output = run_script(&ServiceOptions::default(), "", true);
|
||||||
|
assert_eq!(output.status.code(), Some(19));
|
||||||
|
let text = String::from_utf8(output.stdout).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
text.lines()
|
||||||
|
.filter(|line| line.starts_with("install "))
|
||||||
|
.count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert!(!text.contains("arg="));
|
||||||
|
}
|
||||||
+40
-10
@@ -222,7 +222,16 @@ reload() {{
|
|||||||
/// Generates a FreeBSD rc.d script.
|
/// Generates a FreeBSD rc.d script.
|
||||||
fn generate_freebsd_rc_script(opts: &ServiceOptions) -> String {
|
fn generate_freebsd_rc_script(opts: &ServiceOptions) -> String {
|
||||||
let user = opts.user.unwrap_or("root");
|
let user = opts.user.unwrap_or("root");
|
||||||
let group = opts.group.unwrap_or("wheel");
|
let group = opts.group.unwrap_or("");
|
||||||
|
// The shared PID default cannot support cleanup after dropping privileges.
|
||||||
|
let pid_file = match opts.pid_file {
|
||||||
|
"/var/run/telemt.pid" => "/var/run/telemt/telemt.pid",
|
||||||
|
path => path,
|
||||||
|
};
|
||||||
|
let working_dir = match opts.working_dir {
|
||||||
|
None | Some("/var/lib/telemt") => "/var/db/telemt",
|
||||||
|
Some(path) => path,
|
||||||
|
};
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
r#"#!/bin/sh
|
r#"#!/bin/sh
|
||||||
@@ -237,8 +246,10 @@ fn generate_freebsd_rc_script(opts: &ServiceOptions) -> String {
|
|||||||
# The configuration path can be overridden in rc.conf.
|
# The configuration path can be overridden in rc.conf.
|
||||||
# telemt_config="/etc/telemt/config.toml"
|
# telemt_config="/etc/telemt/config.toml"
|
||||||
# The service user and group can be overridden in rc.conf.
|
# The service user and group can be overridden in rc.conf.
|
||||||
# telemt_user="telemt"
|
# telemt_uid="telemt"
|
||||||
# telemt_group="telemt"
|
# telemt_gid="telemt"
|
||||||
|
# Legacy telemt_user/telemt_group settings remain supported as aliases.
|
||||||
|
# Custom telemt_pidfile parents must already be writable by the service user.
|
||||||
#
|
#
|
||||||
|
|
||||||
. /etc/rc.subr
|
. /etc/rc.subr
|
||||||
@@ -251,27 +262,41 @@ load_rc_config $name
|
|||||||
|
|
||||||
: ${{telemt_enable:="NO"}}
|
: ${{telemt_enable:="NO"}}
|
||||||
: ${{telemt_config:="{config}"}}
|
: ${{telemt_config:="{config}"}}
|
||||||
: ${{telemt_user:="{user}"}}
|
: ${{telemt_uid:="${{telemt_user:-{user}}}"}}
|
||||||
: ${{telemt_group:="{group}"}}
|
: ${{telemt_gid:="${{telemt_group:-{group}}}"}}
|
||||||
: ${{telemt_pidfile:="{pid_file}"}}
|
: ${{telemt_pidfile:="{pid_file}"}}
|
||||||
|
: ${{telemt_data_dir:="{working_dir}"}}
|
||||||
|
|
||||||
|
# An unspecified group follows the selected account, never an implicit wheel grant.
|
||||||
|
if [ -z "${{telemt_gid}}" ]; then
|
||||||
|
telemt_gid="$(id -gn "${{telemt_uid}}")" || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# rc.subr must not drop privileges before Telemt binds privileged sockets.
|
||||||
|
unset telemt_user telemt_group
|
||||||
|
|
||||||
pidfile="${{telemt_pidfile}}"
|
pidfile="${{telemt_pidfile}}"
|
||||||
command="{exe}"
|
command="{exe}"
|
||||||
command_args="--daemon --syslog --pid-file ${{telemt_pidfile}} ${{telemt_config}}"
|
command_args="--daemon --syslog --working-dir \"${{telemt_data_dir}}\" --pid-file \"${{telemt_pidfile}}\""
|
||||||
|
# Root startup must retain privileged network setup without a drop request.
|
||||||
|
if [ "${{telemt_uid}}" != "root" ]; then
|
||||||
|
command_args="${{command_args}} --run-as-user \"${{telemt_uid}}\" --run-as-group \"${{telemt_gid}}\""
|
||||||
|
fi
|
||||||
|
command_args="${{command_args}} \"${{telemt_config}}\""
|
||||||
|
|
||||||
start_precmd="telemt_prestart"
|
start_precmd="telemt_prestart"
|
||||||
reload_cmd="telemt_reload"
|
reload_cmd="telemt_reload"
|
||||||
extra_commands="reload"
|
extra_commands="reload"
|
||||||
|
|
||||||
telemt_prestart() {{
|
telemt_prestart() {{
|
||||||
install -d -o ${{telemt_user}} -g ${{telemt_group}} -m 755 /var/run
|
install -d -o "${{telemt_uid}}" -g "${{telemt_gid}}" -m 755 /var/run/telemt || return $?
|
||||||
install -d -o ${{telemt_user}} -g ${{telemt_group}} -m 755 /var/lib/telemt
|
install -d -o "${{telemt_uid}}" -g "${{telemt_gid}}" -m 755 "${{telemt_data_dir}}" || return $?
|
||||||
}}
|
}}
|
||||||
|
|
||||||
telemt_reload() {{
|
telemt_reload() {{
|
||||||
if [ -f "${{pidfile}}" ]; then
|
if [ -f "${{pidfile}}" ]; then
|
||||||
echo "Reloading ${{name}} configuration."
|
echo "Reloading ${{name}} configuration."
|
||||||
kill -HUP $(cat ${{pidfile}})
|
kill -HUP "$(cat "${{pidfile}}")"
|
||||||
else
|
else
|
||||||
echo "${{name}} is not running."
|
echo "${{name}} is not running."
|
||||||
return 1
|
return 1
|
||||||
@@ -283,7 +308,8 @@ run_rc_command "$1"
|
|||||||
description = opts.description,
|
description = opts.description,
|
||||||
exe = opts.exe_path.display(),
|
exe = opts.exe_path.display(),
|
||||||
config = opts.config_path.display(),
|
config = opts.config_path.display(),
|
||||||
pid_file = opts.pid_file,
|
pid_file = pid_file,
|
||||||
|
working_dir = working_dir,
|
||||||
user = user,
|
user = user,
|
||||||
group = group,
|
group = group,
|
||||||
)
|
)
|
||||||
@@ -343,6 +369,10 @@ You may need to create a service file manually or run telemt directly:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, unix))]
|
||||||
|
// Executable shell checks for the generated FreeBSD service lifecycle.
|
||||||
|
mod freebsd_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user