use std::{fs, path::PathBuf}; use anyhow::Context; use nix::libc::{exit, EXIT_SUCCESS}; use std::os::unix::net::UnixStream as StdUnixStream; use tokio::net::UnixStream as TokioUnixStream; use crate::{ core::common::{UnixUser, DEFAULT_CONFIG_PATH, DEFAULT_SOCKET_PATH}, server::{config::read_config_from_path, server_loop::handle_requests_for_single_session}, }; // TODO: this function is security critical, it should be integration tested // in isolation. /// Drop privileges to the real user and group of the process. /// If the process is not running with elevated privileges, this function /// is a no-op. pub fn drop_privs() -> anyhow::Result<()> { log::debug!("Dropping privileges"); let real_uid = nix::unistd::getuid(); let real_gid = nix::unistd::getgid(); nix::unistd::setuid(real_uid).context("Failed to drop privileges")?; nix::unistd::setgid(real_gid).context("Failed to drop privileges")?; debug_assert_eq!(nix::unistd::getuid(), real_uid); debug_assert_eq!(nix::unistd::getgid(), real_gid); log::debug!("Privileges dropped successfully"); Ok(()) } /// This function is used to bootstrap the connection to the server. /// This can happen in two ways: /// /// 1. If a socket path is provided, or exists in the default location, /// the function will connect to the socket and authenticate with the /// server to ensure that the server knows the uid of the client. /// /// 2. If a config path is provided, or exists in the default location, /// and the config is readable, the function will assume it is either /// setuid or setgid, and will fork a child process to run the server /// with the provided config. The server will exit silently by itself /// when it is done, and this function will only return for the client /// with the socket for the server. /// /// If neither of these options are available, the function will fail. pub fn bootstrap_server_connection_and_drop_privileges( server_socket_path: Option, config_path: Option, ) -> anyhow::Result { if server_socket_path.is_some() && config_path.is_some() { anyhow::bail!("Cannot provide both a socket path and a config path"); } log::debug!("Starting the server connection bootstrap process"); let socket = bootstrap_server_connection(server_socket_path, config_path)?; drop_privs()?; Ok(socket) } /// Inner function for [`bootstrap_server_connection_and_drop_privileges`]. /// See that function for more information. fn bootstrap_server_connection( socket_path: Option, config_path: Option, ) -> anyhow::Result { // TODO: ensure this is both readable and writable if let Some(socket_path) = socket_path { log::debug!("Connecting to socket at {:?}", socket_path); return match StdUnixStream::connect(socket_path) { Ok(socket) => Ok(socket), Err(e) => match e.kind() { std::io::ErrorKind::NotFound => Err(anyhow::anyhow!("Socket not found")), std::io::ErrorKind::PermissionDenied => Err(anyhow::anyhow!("Permission denied")), _ => Err(anyhow::anyhow!("Failed to connect to socket: {}", e)), }, }; } if let Some(config_path) = config_path { // ensure config exists and is readable if fs::metadata(&config_path).is_err() { return Err(anyhow::anyhow!("Config file not found or not readable")); } log::debug!("Starting server with config at {:?}", config_path); return invoke_server_with_config(config_path); } if fs::metadata(DEFAULT_SOCKET_PATH).is_ok() { log::debug!("Connecting to default socket at {:?}", DEFAULT_SOCKET_PATH); return match StdUnixStream::connect(DEFAULT_SOCKET_PATH) { Ok(socket) => Ok(socket), Err(e) => match e.kind() { std::io::ErrorKind::NotFound => Err(anyhow::anyhow!("Socket not found")), std::io::ErrorKind::PermissionDenied => Err(anyhow::anyhow!("Permission denied")), _ => Err(anyhow::anyhow!("Failed to connect to socket: {}", e)), }, }; } let config_path = PathBuf::from(DEFAULT_CONFIG_PATH); if fs::metadata(&config_path).is_ok() { log::debug!("Starting server with default config at {:?}", config_path); return invoke_server_with_config(config_path); } anyhow::bail!("No socket path or config path provided, and no default socket or config found"); } // TODO: we should somehow ensure that the forked process is killed on completion, // just in case the client does not behave properly. /// Fork a child process to run the server with the provided config. /// The server will exit silently by itself when it is done, and this function /// will only return for the client with the socket for the server. fn invoke_server_with_config(config_path: PathBuf) -> anyhow::Result { let (server_socket, client_socket) = StdUnixStream::pair()?; let unix_user = UnixUser::from_uid(nix::unistd::getuid().as_raw())?; match (unsafe { nix::unistd::fork() }).context("Failed to fork")? { nix::unistd::ForkResult::Parent { child } => { log::debug!("Forked child process with PID {}", child); Ok(client_socket) } nix::unistd::ForkResult::Child => { log::debug!("Running server in child process"); match run_forked_server(config_path, server_socket, unix_user) { Err(e) => Err(e), Ok(_) => unreachable!(), } } } } /// Run the server in the forked child process. /// This function will not return, but will exit the process with a success code. fn run_forked_server( config_path: PathBuf, server_socket: StdUnixStream, unix_user: UnixUser, ) -> anyhow::Result<()> { let config = read_config_from_path(Some(config_path))?; let result: anyhow::Result<()> = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap() .block_on(async { let socket = TokioUnixStream::from_std(server_socket)?; handle_requests_for_single_session(socket, &unix_user, &config).await?; Ok(()) }); result?; unsafe { exit(EXIT_SUCCESS); } }