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 | |
| parent | 8f6f61ebd6a013f657a2523dbe4b4cfa1a1cb462 (diff) | |
| download | mullvadvpn-308832d0df052dd4433bda2cc5c586ca9745c249.tar.xz mullvadvpn-308832d0df052dd4433bda2cc5c586ca9745c249.zip | |
Add basic windows service
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | app/main.js | 19 | ||||
| -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 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 1 | ||||
| -rw-r--r-- | talpid-core/src/lib.rs | 1 | ||||
| -rw-r--r-- | talpid-core/src/process/openvpn.rs | 19 | ||||
| -rw-r--r-- | windows-service/examples/simple_service.rs | 2 | ||||
| -rw-r--r-- | windows-service/src/service.rs | 8 |
11 files changed, 347 insertions, 11 deletions
diff --git a/Cargo.lock b/Cargo.lock index b9a35b29cc..6c10aa9925 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -734,6 +734,7 @@ dependencies = [ "tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "windows-service 0.1.0", ] [[package]] @@ -1274,6 +1275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "talpid-core" version = "0.1.0" dependencies = [ + "atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "duct 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/app/main.js b/app/main.js index 0c5eb7ec73..df7f9fbf50 100644 --- a/app/main.js +++ b/app/main.js @@ -41,7 +41,11 @@ const appDelegate = { log.info('Running version', version); - appDelegate._startBackend(); + // Only macOS builds still launch the daemon manually. + // On other platforms mullvad-daemon already runs as a system service. + if (process.platform === 'darwin') { + appDelegate._startBackend(); + } app.on('window-all-closed', () => appDelegate.onAllWindowsClosed()); app.on('ready', () => appDelegate.onReady()); @@ -95,15 +99,18 @@ const appDelegate = { ipcMain.on('hide-window', () => window.hide()); window.loadURL('file://' + path.join(__dirname, 'index.html')); - if (process.platform === 'linux') { + + // Since macOS still runs the daemon manually it has to shut it down. + // On other platforms closing the app only disconnects the tunnel. + if (process.platform === 'darwin') { window.on('close', () => { - log.debug('The browser window is closing, shutting down the tunnel...'); - window.webContents.send('disconnect'); + log.debug('The browser window is closing, shutting down the daemon...'); + window.webContents.send('shutdown'); }); } else { window.on('close', () => { - log.debug('The browser window is closing, shutting down the daemon...'); - window.webContents.send('shutdown'); + log.debug('The browser window is closing, shutting down the tunnel...'); + window.webContents.send('disconnect'); }); } 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, + } +} diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index addb9f61d7..c9e4da653c 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -6,6 +6,7 @@ description = "Privacy preserving and secure VPN client library" license = "GPL-3.0" [dependencies] +atty = "0.2" duct = "0.10" error-chain = "0.11" jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc", tag = "v8.0.1" } diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs index fc85584267..6c6e907255 100644 --- a/talpid-core/src/lib.rs +++ b/talpid-core/src/lib.rs @@ -10,6 +10,7 @@ //! GNU General Public License as published by the Free Software Foundation, either version 3 of //! the License, or (at your option) any later version. +extern crate atty; extern crate duct; #[macro_use] diff --git a/talpid-core/src/process/openvpn.rs b/talpid-core/src/process/openvpn.rs index f315e86eb5..734918853f 100644 --- a/talpid-core/src/process/openvpn.rs +++ b/talpid-core/src/process/openvpn.rs @@ -1,3 +1,4 @@ +use atty; use duct; use std::ffi::{OsStr, OsString}; @@ -106,7 +107,23 @@ impl OpenVpnCommand { /// Build a runnable expression from the current state of the command. pub fn build(&self) -> duct::Expression { debug!("Building expression: {}", &self); - duct::cmd(&self.openvpn_bin, self.get_arguments()).unchecked() + + let mut cmd = duct::cmd(&self.openvpn_bin, self.get_arguments()).unchecked(); + + // Prevent forwarding the stdio when it's not available. + if atty::is(atty::Stream::Stdin) { + cmd = cmd.stdin_null(); + } + + if atty::is(atty::Stream::Stdout) { + cmd = cmd.stdout_null(); + } + + if atty::is(atty::Stream::Stderr) { + cmd = cmd.stderr_null(); + } + + cmd } /// Sets extra options diff --git a/windows-service/examples/simple_service.rs b/windows-service/examples/simple_service.rs index d175e69564..b1a717a8c1 100644 --- a/windows-service/examples/simple_service.rs +++ b/windows-service/examples/simple_service.rs @@ -163,7 +163,7 @@ mod simple_service { define_windows_service!(service_main, handle_service_main); pub fn handle_service_main(arguments: Vec<OsString>) { - // Create a shutdown channel to release this thread when stopping the service + // Create an event channel to funnel events to worker. let (event_tx, event_rx) = mpsc::channel(); info!("Received arguments: {:?}", arguments); diff --git a/windows-service/src/service.rs b/windows-service/src/service.rs index f609221b8c..1898cf8c1c 100644 --- a/windows-service/src/service.rs +++ b/windows-service/src/service.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use std::time::Duration; use std::{io, mem}; -use winapi::shared::winerror::ERROR_SERVICE_SPECIFIC_ERROR; +use winapi::shared::winerror::{ERROR_SERVICE_SPECIFIC_ERROR, NO_ERROR}; use winapi::um::{winnt, winsvc}; mod errors { @@ -242,6 +242,12 @@ impl ServiceExitCode { } } +impl Default for ServiceExitCode { + fn default() -> Self { + ServiceExitCode::Win32(NO_ERROR) + } +} + impl<'a> From<&'a winsvc::SERVICE_STATUS> for ServiceExitCode { fn from(service_status: &'a winsvc::SERVICE_STATUS) -> Self { if service_status.dwWin32ExitCode == ERROR_SERVICE_SPECIFIC_ERROR { |
