diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-04-19 18:55:31 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-04-23 21:51:59 +0200 |
| commit | 308832d0df052dd4433bda2cc5c586ca9745c249 (patch) | |
| tree | fb923fc7a264149837fe36d442b920bed8874a48 /mullvad-daemon | |
| parent | 8f6f61ebd6a013f657a2523dbe4b4cfa1a1cb462 (diff) | |
| download | mullvadvpn-308832d0df052dd4433bda2cc5c586ca9745c249.tar.xz mullvadvpn-308832d0df052dd4433bda2cc5c586ca9745c249.zip | |
Add basic windows service
Diffstat (limited to 'mullvad-daemon')
| -rw-r--r-- | mullvad-daemon/Cargo.toml | 1 | ||||
| -rw-r--r-- | mullvad-daemon/src/cli.rs | 25 | ||||
| -rw-r--r-- | mullvad-daemon/src/main.rs | 31 | ||||
| -rw-r--r-- | mullvad-daemon/src/system_service.rs | 249 |
4 files changed, 304 insertions, 2 deletions
diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml index 3ff37cbaf9..581784c8ce 100644 --- a/mullvad-daemon/Cargo.toml +++ b/mullvad-daemon/Cargo.toml @@ -39,6 +39,7 @@ simple-signal = "1.1" [target.'cfg(windows)'.dependencies] ctrlc = "3.0" +windows-service = { path = "../windows-service" } [dev-dependencies] assert_matches = "1.0" diff --git a/mullvad-daemon/src/cli.rs b/mullvad-daemon/src/cli.rs index f3c5773912..f70d69f947 100644 --- a/mullvad-daemon/src/cli.rs +++ b/mullvad-daemon/src/cli.rs @@ -12,6 +12,8 @@ pub struct Config { pub resource_dir: Option<PathBuf>, pub require_auth: bool, pub log_stdout_timestamps: bool, + pub run_as_service: bool, + pub register_service: bool, } pub fn get_config() -> Config { @@ -29,6 +31,9 @@ pub fn get_config() -> Config { let require_auth = !matches.is_present("disable_rpc_auth"); let log_stdout_timestamps = !matches.is_present("disable_stdout_timestamps"); + let run_as_service = cfg!(windows) && matches.is_present("run_as_service"); + let register_service = cfg!(windows) && matches.is_present("register_service"); + Config { log_level, log_file, @@ -36,11 +41,13 @@ pub fn get_config() -> Config { resource_dir, require_auth, log_stdout_timestamps, + run_as_service, + register_service, } } fn create_app() -> App<'static, 'static> { - App::new(crate_name!()) + let app = App::new(crate_name!()) .version(version::current()) .author(crate_authors!()) .about(crate_description!()) @@ -80,5 +87,19 @@ fn create_app() -> App<'static, 'static> { Arg::with_name("disable_stdout_timestamps") .long("disable-stdout-timestamps") .help("Don't log timestamps when logging to stdout, useful when running as a systemd service") - ) + ); + + if cfg!(windows) { + app.arg( + Arg::with_name("run_as_service") + .long("run-as-service") + .help("Run as a system service. On Windows this option must be used when running a system service"), + ).arg( + Arg::with_name("register_service") + .long("register-service") + .help("Register itself as a system service"), + ) + } else { + app + } } diff --git a/mullvad-daemon/src/main.rs b/mullvad-daemon/src/main.rs index 9594448a74..897a920737 100644 --- a/mullvad-daemon/src/main.rs +++ b/mullvad-daemon/src/main.rs @@ -41,6 +41,10 @@ extern crate talpid_core; extern crate talpid_ipc; extern crate talpid_types; +#[cfg(windows)] +#[macro_use] +extern crate windows_service; + mod account_history; mod cli; mod geoip; @@ -51,6 +55,7 @@ mod rpc_address_file; mod rpc_uniqueness_check; mod settings; mod shutdown; +mod system_service; mod version; use app_dirs::AppInfo; @@ -855,7 +860,33 @@ fn run() -> Result<()> { config.log_stdout_timestamps, ).chain_err(|| "Unable to initialize logger")?; log_version(); + run_platform(config) +} + +#[cfg(windows)] +fn run_platform(config: cli::Config) -> Result<()> { + if config.run_as_service { + system_service::run() + } else { + if config.register_service { + let install_result = + system_service::install_service().chain_err(|| "Unable to install the service"); + if install_result.is_ok() { + println!("Installed the service."); + } + install_result + } else { + run_standalone(config) + } + } +} + +#[cfg(not(windows))] +fn run_platform(config: cli::Config) -> Result<()> { + run_standalone(config) +} +fn run_standalone(config: cli::Config) -> Result<()> { if !running_as_admin() { warn!("Running daemon as a non-administrator user, clients might refuse to connect"); } diff --git a/mullvad-daemon/src/system_service.rs b/mullvad-daemon/src/system_service.rs new file mode 100644 index 0000000000..5c6e612cc5 --- /dev/null +++ b/mullvad-daemon/src/system_service.rs @@ -0,0 +1,249 @@ +#![cfg(windows)] + +use std::ffi::OsString; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{mpsc, Arc}; +use std::time::Duration; +use std::{env, io, thread}; + +use cli; +use error_chain::ChainedError; +use windows_service::service::{ServiceAccess, ServiceControl, ServiceControlAccept, + ServiceErrorControl, ServiceExitCode, ServiceInfo, + ServiceStartType, ServiceState, ServiceStatus, ServiceType}; +use windows_service::service_control_handler::{self, ServiceControlHandlerResult, + ServiceStatusHandle}; +use windows_service::service_dispatcher; +use windows_service::service_manager::{ServiceManager, ServiceManagerAccess}; + +use super::{get_resource_dir, Daemon, DaemonShutdownHandle, Result, ResultExt}; + +static SERVICE_NAME: &'static str = "MullvadVPN"; +static SERVICE_DISPLAY_NAME: &'static str = "Mullvad VPN Service"; +static SERVICE_TYPE: ServiceType = ServiceType::OwnProcess; + +pub fn run() -> Result<()> { + // Start the service dispatcher. + // This will block current thread until the service stopped and spawn `service_main` on a + // background thread. + service_dispatcher::start_dispatcher(SERVICE_NAME, service_main) + .chain_err(|| "Failed to start a service dispatcher") +} + +define_windows_service!(service_main, handle_service_main); + +pub fn handle_service_main(arguments: Vec<OsString>) { + info!("Service started."); + match run_service(arguments) { + Ok(_) => info!("Service stopped."), + Err(ref e) => error!("Service stopped with error: {}", e.display_chain()), + }; +} + +fn run_service(_arguments: Vec<OsString>) -> Result<()> { + let config = cli::get_config(); + let (event_tx, event_rx) = mpsc::channel(); + + // Register service event handler + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + // Notifies a service to report its current status information to the service + // control manager. Always return NO_ERROR even if not implemented. + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + + ServiceControl::Shutdown | ServiceControl::Stop => { + event_tx.send(control_event).unwrap(); + ServiceControlHandlerResult::NoError + } + + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + let status_handle = + service_control_handler::register_control_handler(SERVICE_NAME, event_handler) + .chain_err(|| "Failed to register a service control handler")?; + let mut persistent_service_status = PersistentServiceStatus::new(status_handle); + persistent_service_status + .set_pending_start(Duration::from_secs(1)) + .unwrap(); + + let resource_dir = get_resource_dir(); + let daemon = Daemon::new(config.tunnel_log_file, resource_dir, config.require_auth) + .chain_err(|| "Unable to initialize daemon")?; + let shutdown_handle = daemon.shutdown_handle(); + + // Register monitor that translates `ServiceControl` to Daemon events + start_event_monitor(persistent_service_status.clone(), shutdown_handle, event_rx); + + persistent_service_status.set_running().unwrap(); + + let result = daemon.run(); + + // TODO: report correct exit code back after running a daemon. + persistent_service_status + .set_stopped(ServiceExitCode::default()) + .unwrap(); + + result +} + +/// Start event monitor thread that polls for `ServiceControl` and translates them into calls to +/// Daemon. +fn start_event_monitor( + mut persistent_service_status: PersistentServiceStatus, + shutdown_handle: DaemonShutdownHandle, + event_rx: mpsc::Receiver<ServiceControl>, +) -> thread::JoinHandle<()> { + thread::spawn(move || { + for event in event_rx { + match event { + ServiceControl::Stop | ServiceControl::Shutdown => { + persistent_service_status + .set_pending_stop(Duration::from_secs(3)) + .unwrap(); + + shutdown_handle.shutdown(); + } + _ => (), + } + } + }) +} + + +/// Service status helper with persistent checkpoint counter. +#[derive(Debug, Clone)] +struct PersistentServiceStatus { + status_handle: ServiceStatusHandle, + checkpoint_counter: Arc<AtomicUsize>, +} + +impl PersistentServiceStatus { + fn new(status_handle: ServiceStatusHandle) -> Self { + PersistentServiceStatus { + status_handle, + checkpoint_counter: Arc::new(AtomicUsize::new(1)), + } + } + + /// Tell the system that the service is pending start and provide the time estimate until + /// initialization is complete. + fn set_pending_start(&mut self, wait_hint: Duration) -> io::Result<()> { + self.report_status( + ServiceState::StartPending, + wait_hint, + ServiceExitCode::default(), + ) + } + + /// Tell the system that the service is running. + fn set_running(&mut self) -> io::Result<()> { + self.report_status( + ServiceState::Running, + Duration::default(), + ServiceExitCode::default(), + ) + } + + /// Tell the system that the service is pending stop and provide the time estimate until the + /// service is stopped. + fn set_pending_stop(&mut self, wait_hint: Duration) -> io::Result<()> { + self.report_status( + ServiceState::StopPending, + wait_hint, + ServiceExitCode::default(), + ) + } + + /// Tell the system that the service is stopped and provide the exit code. + fn set_stopped(&mut self, exit_code: ServiceExitCode) -> io::Result<()> { + self.report_status(ServiceState::Stopped, Duration::default(), exit_code) + } + + /// Private helper to report the service status update. + fn report_status( + &mut self, + next_state: ServiceState, + wait_hint: Duration, + exit_code: ServiceExitCode, + ) -> io::Result<()> { + // Automatically bump the checkpoint when updating the pending events to tell the system + // that the service is making a progress in transition from pending to final state. + // `wait_hint` should reflect the estimated time for transition to complete. + let checkpoint = match next_state { + ServiceState::StartPending + | ServiceState::StopPending + | ServiceState::ContinuePending + | ServiceState::PausePending => self.checkpoint_counter.fetch_add(1, Ordering::SeqCst), + _ => 0, + }; + + let service_status = ServiceStatus { + service_type: SERVICE_TYPE, + current_state: next_state, + controls_accepted: accepted_controls_by_state(next_state), + exit_code: exit_code, + checkpoint: checkpoint as u32, + wait_hint: wait_hint, + }; + + debug!( + "Update service status: {:?}, checkpoint: {}, wait_hint: {:?}", + service_status.current_state, service_status.checkpoint, service_status.wait_hint + ); + + self.status_handle.set_service_status(service_status) + } +} + +/// Returns the list of accepted service events at each stage of the service lifecycle. +fn accepted_controls_by_state(state: ServiceState) -> ServiceControlAccept { + match state { + ServiceState::StartPending | ServiceState::PausePending | ServiceState::ContinuePending => { + ServiceControlAccept::empty() + } + ServiceState::Running => ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, + ServiceState::Paused => ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, + ServiceState::StopPending | ServiceState::Stopped => ServiceControlAccept::empty(), + } +} + +pub fn install_service() -> Result<()> { + let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; + let service_manager = ServiceManager::local_computer(None::<&str>, manager_access) + .chain_err(|| "Unable to connect to service manager")?; + service_manager + .create_service(get_service_info(), ServiceAccess::empty()) + .map(|_| ()) + .chain_err(|| "Unable to create a service") +} + +fn get_service_info() -> ServiceInfo { + let windows_directory = ::std::env::var_os("WINDIR").unwrap(); + let service_log_file = PathBuf::from(&windows_directory) + .join("Temp") + .join("mullvad-service.log"); + let tunnel_log_file = PathBuf::from(&windows_directory) + .join("Temp") + .join("mullvad-openvpn.log"); + + ServiceInfo { + name: OsString::from(SERVICE_NAME), + display_name: OsString::from(SERVICE_DISPLAY_NAME), + service_type: SERVICE_TYPE, + start_type: ServiceStartType::AutoStart, + error_control: ServiceErrorControl::Normal, + executable_path: env::current_exe().unwrap(), + launch_arguments: vec![ + OsString::from("--log"), + OsString::from(service_log_file), + OsString::from("--tunnel-log"), + OsString::from(tunnel_log_file), + OsString::from("--run-as-service"), + OsString::from("-v"), + ], + account_name: None, // run as System + account_password: None, + } +} |
