summaryrefslogtreecommitdiffhomepage
path: root/mullvad-daemon
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-04-19 18:55:31 +0200
committerAndrej Mihajlov <and@mullvad.net>2018-04-23 21:51:59 +0200
commit308832d0df052dd4433bda2cc5c586ca9745c249 (patch)
treefb923fc7a264149837fe36d442b920bed8874a48 /mullvad-daemon
parent8f6f61ebd6a013f657a2523dbe4b4cfa1a1cb462 (diff)
downloadmullvadvpn-308832d0df052dd4433bda2cc5c586ca9745c249.tar.xz
mullvadvpn-308832d0df052dd4433bda2cc5c586ca9745c249.zip
Add basic windows service
Diffstat (limited to 'mullvad-daemon')
-rw-r--r--mullvad-daemon/Cargo.toml1
-rw-r--r--mullvad-daemon/src/cli.rs25
-rw-r--r--mullvad-daemon/src/main.rs31
-rw-r--r--mullvad-daemon/src/system_service.rs249
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,
+ }
+}