Fix FreeBSD service runtime ownership and privilege drop #913

This commit is contained in:
Alexey
2026-10-07 20:38:46 +03:00
parent deafa4a6cf
commit 7053a7faf6
3 changed files with 283 additions and 12 deletions
+55 -2
View File
@@ -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);
}
}
+188
View File
@@ -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
View File
@@ -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::*;