summaryrefslogtreecommitdiffhomepage
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
parent8f6f61ebd6a013f657a2523dbe4b4cfa1a1cb462 (diff)
downloadmullvadvpn-308832d0df052dd4433bda2cc5c586ca9745c249.tar.xz
mullvadvpn-308832d0df052dd4433bda2cc5c586ca9745c249.zip
Add basic windows service
-rw-r--r--Cargo.lock2
-rw-r--r--app/main.js19
-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
-rw-r--r--talpid-core/Cargo.toml1
-rw-r--r--talpid-core/src/lib.rs1
-rw-r--r--talpid-core/src/process/openvpn.rs19
-rw-r--r--windows-service/examples/simple_service.rs2
-rw-r--r--windows-service/src/service.rs8
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 {