diff options
| author | David Lönnhager <david.l@mullvad.net> | 2020-03-16 13:30:26 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2020-03-16 13:30:26 +0100 |
| commit | e4985cf5e337c6be16f3af550fa5678121800f8c (patch) | |
| tree | 3f311e7ae7c181b417ca6be5cca418c9f362f738 | |
| parent | f57b5a3c172355b8677fdd66a6e452fa1e730ced (diff) | |
| parent | 329e52f9cceb64ec2591383b56ec7dcfba11cdac (diff) | |
| download | mullvadvpn-e4985cf5e337c6be16f3af550fa5678121800f8c.tar.xz mullvadvpn-e4985cf5e337c6be16f3af550fa5678121800f8c.zip | |
Merge branch 'block-on-upgrade'
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | Cargo.lock | 15 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rwxr-xr-x | build.sh | 1 | ||||
| -rw-r--r-- | dist-assets/windows/installer.nsh | 99 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 83 | ||||
| -rw-r--r-- | mullvad-daemon/src/management_interface.rs | 10 | ||||
| -rw-r--r-- | mullvad-ipc-client/src/lib.rs | 4 | ||||
| -rw-r--r-- | mullvad-setup/Cargo.toml | 33 | ||||
| -rw-r--r-- | mullvad-setup/build.rs | 18 | ||||
| -rw-r--r-- | mullvad-setup/src/main.rs | 80 | ||||
| -rwxr-xr-x | version_metadata.sh | 3 |
12 files changed, 307 insertions, 42 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f82e056c..1cbb188f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,8 @@ Line wrap the file at 100 chars. Th #### Windows - Fix issue in daemon where the `block_when_disconnected` setting was sometimes not honored when stopping the daemon. I.e. traffic could flow freely after the daemon was stopped. +- When upgrading or reinstalling while connected, exit the daemon in a blocking state to prevent + unintended leaks. This only affects upgrades from this release. ## [2020.3] - 2020-02-20 diff --git a/Cargo.lock b/Cargo.lock index 0a43069b74..22ed06c8d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1386,6 +1386,21 @@ dependencies = [ ] [[package]] +name = "mullvad-setup" +version = "2020.3.0" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "err-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mullvad-ipc-client 0.1.0", + "mullvad-paths 0.1.0", + "talpid-core 0.1.0", + "talpid-types 0.1.0", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winres 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "mullvad-tests" version = "0.1.0" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml index c25bc54279..bbaaee503a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "mullvad-daemon", "mullvad-cli", + "mullvad-setup", "mullvad-problem-report", "mullvad-ipc-client", "mullvad-jni", @@ -160,6 +160,7 @@ elif [[ ("$(uname -s)" == "MINGW"*) ]]; then mullvad.exe mullvad-problem-report.exe talpid_openvpn_plugin.dll + mullvad-setup.exe ) fi for binary in ${binaries[*]}; do diff --git a/dist-assets/windows/installer.nsh b/dist-assets/windows/installer.nsh index c7adccc49e..0e5d1e6646 100644 --- a/dist-assets/windows/installer.nsh +++ b/dist-assets/windows/installer.nsh @@ -40,22 +40,7 @@ # electron-builder uses a GUID here rather than the application name. !define INSTALL_REGISTRY_KEY "Software\${PRODUCT_NAME}" -# -# BreakInstallation -# -# Aborting the customization step does not undo previous steps taken -# by the installer (copy files, create shortcut, etc) -# -# Therefore we have to break the installed application to -# prevent users from running a half-installed product -# -!macro BreakInstallation - - Delete "$INSTDIR\mullvad vpn.exe" - -!macroend - -!define BreakInstallation '!insertmacro "BreakInstallation"' +!define BLOCK_OUTBOUND_IPV4_FILTER_GUID "{a81c5411-0fd0-43a9-a9be-313f299de64f}" # # ExtractTapDriver @@ -649,8 +634,7 @@ ${If} $R0 != 0 MessageBox MB_OK "Fatal error during driver installation: $R0" - ${BreakInstallation} - Abort + Goto customInstall_abort_installation ${EndIf} ${ExtractWintun} @@ -658,21 +642,54 @@ ${If} $R0 != 0 MessageBox MB_OK "$R0" - ${BreakInstallation} - Abort + Goto customInstall_abort_installation ${EndIf} ${InstallService} ${If} $R0 != 0 MessageBox MB_OK "$R0" - ${BreakInstallation} - Abort + Goto customInstall_abort_installation ${EndIf} ${AddCLIToEnvironPath} ${InstallTrayIcon} + Goto customInstall_skip_abort + + customInstall_abort_installation: + + # Aborting the customization step does not undo previous steps taken + # by the installer (copy files, create shortcut, etc) + # + # Therefore we have to break the installed application to + # prevent users from running a half-installed product + # + Delete "$INSTDIR\mullvad vpn.exe" + + nsExec::ExecToStack '"$SYSDIR\netsh.exe" wfp show security FILTER ${BLOCK_OUTBOUND_IPV4_FILTER_GUID}' + Pop $0 + Pop $1 + + ${If} $0 == 0 + MessageBox MB_ICONEXCLAMATION|MB_YESNO "Do you wish to unblock your internet access? Doing so will leave you with an unsecure connection." IDNO customInstall_abortInstallation_skip_firewall_revert + + SetOutPath "$TEMP" + File "${BUILD_RESOURCES_DIR}\mullvad-setup.exe" + File "${BUILD_RESOURCES_DIR}\..\windows\winfw\bin\x64-Release\winfw.dll" + nsExec::ExecToStack '"$TEMP\mullvad-setup.exe" reset-firewall' + Pop $0 + Pop $1 + + log::Log "Resetting firewall: $0 $1" + ${EndIf} + + customInstall_abortInstallation_skip_firewall_revert: + + Abort + + customInstall_skip_abort: + Pop $R0 !macroend @@ -694,20 +711,6 @@ Push $0 Push $1 - nsExec::ExecToStack '"$SYSDIR\sc.exe" stop mullvadvpn' - - # Discard return value - Pop $0 - - Sleep 5000 - - nsExec::ExecToStack '"$SYSDIR\sc.exe" delete mullvadvpn' - - # Discard return value - Pop $0 - - Sleep 1000 - # Check command line arguments Var /GLOBAL FullUninstall @@ -722,6 +725,30 @@ ${EndIf} Pop $FullUninstall + ${If} $FullUninstall != 1 + # Save the target tunnel state if we're upgrading + SetOutPath "$TEMP" + File "${BUILD_RESOURCES_DIR}\mullvad-setup.exe" + File "${BUILD_RESOURCES_DIR}\..\windows\winfw\bin\x64-Release\winfw.dll" + nsExec::ExecToStack '"$TEMP\mullvad-setup.exe" prepare-restart' + Pop $0 + Pop $1 + ${EndIf} + + nsExec::ExecToStack '"$SYSDIR\sc.exe" stop mullvadvpn' + + # Discard return value + Pop $0 + + Sleep 5000 + + nsExec::ExecToStack '"$SYSDIR\sc.exe" delete mullvadvpn' + + # Discard return value + Pop $0 + + Sleep 1000 + log::Log "Running uninstaller for ${PRODUCT_NAME} ${VERSION}" ${RemoveCLIFromEnvironPath} diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 74383b43a4..bca668bc28 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -45,6 +45,7 @@ use settings::Settings; #[cfg(not(target_os = "android"))] use std::path::Path; use std::{ + fs::{self, File}, io, marker::PhantomData, mem, @@ -68,6 +69,8 @@ use talpid_types::{ #[path = "wireguard.rs"] mod wireguard; +const TARGET_START_STATE_FILE: &str = "target-start-state.json"; + /// FIXME(linus): This is here just because the futures crate has deprecated it and jsonrpc_core /// did not introduce their own yet (https://github.com/paritytech/jsonrpc/pull/196). /// Remove this and use the one in jsonrpc_core when that is released. @@ -125,6 +128,12 @@ pub enum Error { #[cfg(target_os = "windows")] #[error(display = "Failed to read dir entries")] ReadDirError(#[error(source)] io::Error), + + #[error(display = "Failed to read cached target tunnel state")] + ReadCachedTargetState(#[error(source)] serde_json::Error), + + #[error(display = "Failed to open cached target tunnel state")] + OpenCachedTargetState(#[error(source)] io::Error), } /// Enum representing commands that can be sent to the daemon. @@ -206,6 +215,9 @@ pub enum DaemonCommand { FactoryReset(oneshot::Sender<()>), /// Makes the daemon exit the main loop and quit. Shutdown, + /// Saves the target tunnel state and enters a blocking state. The state is restored + /// upon restart. + PrepareRestart, } /// All events that can happen in the daemon. Sent from various threads and exposed interfaces. @@ -438,6 +450,7 @@ pub struct Daemon<L: EventListener> { shutdown_callbacks: Vec<Box<dyn FnOnce()>>, /// oneshot channel that completes once the tunnel state machine has been shut down tunnel_state_machine_shutdown_signal: oneshot::Receiver<()>, + cache_dir: PathBuf, } impl<L> Daemon<L> @@ -506,6 +519,26 @@ where ) .map_err(Error::LoadAccountHistory)?; + // Restore the tunnel to a previous state + let target_cache = cache_dir.join(TARGET_START_STATE_FILE); + let cached_target_state: Option<TargetState> = match File::open(&target_cache) { + Ok(handle) => serde_json::from_reader(io::BufReader::new(handle)) + .map(Some) + .map_err(Error::ReadCachedTargetState), + Err(e) => { + if e.kind() == io::ErrorKind::NotFound { + Ok(None) + } else { + Err(Error::OpenCachedTargetState(e)) + } + } + }?; + if cached_target_state.is_some() { + let _ = fs::remove_file(target_cache).map_err(|e| { + error!("Cannot delete target tunnel state cache: {}", e); + }); + } + let tunnel_parameters_generator = MullvadTunnelParametersGenerator { tx: internal_event_tx.clone(), }; @@ -515,7 +548,7 @@ where tunnel_parameters_generator, log_dir, resource_dir, - cache_dir, + cache_dir.clone(), internal_event_tx.to_specialized_sender(), tunnel_state_machine_shutdown_tx, #[cfg(target_os = "android")] @@ -532,10 +565,23 @@ where // Attempt to download a fresh relay list relay_selector.update(); + let initial_target_state = if settings.get_account_token().is_some() { + if settings.get_auto_connect() { + // Note: Auto-connect overrides the cached target state + info!("Automatically connecting since auto-connect is turned on"); + TargetState::Secured + } else { + info!("Restoring cached target state"); + cached_target_state.unwrap_or(TargetState::Unsecured) + } + } else { + TargetState::Unsecured + }; + let mut daemon = Daemon { tunnel_command_tx, tunnel_state: TunnelState::Disconnected, - target_state: TargetState::Unsecured, + target_state: initial_target_state, state: DaemonExecutionState::Running, rx: internal_event_rx.wait(), tx: internal_event_tx, @@ -554,6 +600,7 @@ where app_version_info, shutdown_callbacks: vec![], tunnel_state_machine_shutdown_signal, + cache_dir, }; daemon.ensure_wireguard_keys_for_current_account(); @@ -578,9 +625,8 @@ where /// Consume the `Daemon` and run the main event loop. Blocks until an error happens or a /// shutdown event is received. pub fn run(mut self) -> Result<(), Error> { - if self.settings.get_auto_connect() && self.settings.get_account_token().is_some() { - info!("Automatically connecting since auto-connect is turned on"); - self.set_target_state(TargetState::Secured); + if self.target_state == TargetState::Secured { + self.connect_tunnel(); } while let Some(Ok(event)) = self.rx.next() { self.handle_event(event); @@ -939,6 +985,7 @@ where #[cfg(not(target_os = "android"))] FactoryReset(tx) => self.on_factory_reset(tx), Shutdown => self.trigger_shutdown_event(), + PrepareRestart => self.on_prepare_restart(), } } @@ -1658,6 +1705,31 @@ where self.disconnect_tunnel(); } + fn on_prepare_restart(&mut self) { + // TODO: See if this can be made to also shut down the daemon + // without causing the service to be restarted. + + // Cache the current target state + let cache_file = self.cache_dir.join(TARGET_START_STATE_FILE); + log::debug!("Saving tunnel target state to {}", cache_file.display()); + match File::create(&cache_file) { + Ok(handle) => { + if let Err(e) = + serde_json::to_writer(io::BufWriter::new(handle), &self.target_state) + { + log::error!("Failed to serialize target start state: {}", e); + } + } + Err(e) => { + log::error!("Failed to save target start state: {}", e); + } + } + + if self.target_state == TargetState::Secured { + self.send_tunnel_command(TunnelCommand::BlockWhenDisconnected(true)); + } + } + /// Set the target state of the client. If it changed trigger the operations needed to /// progress towards that state. /// Returns an error if trying to set secured state, but no account token is present. @@ -1721,7 +1793,6 @@ where #[cfg(not(target_os = "android"))] fn clear_directory(path: &Path) -> Result<(), Error> { - use std::fs; #[cfg(not(target_os = "windows"))] { fs::remove_dir_all(path) diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index cf5d403e47..e0f7a94b4e 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -108,6 +108,11 @@ build_rpc_trait! { #[rpc(meta, name = "shutdown")] fn shutdown(&self, Self::Metadata) -> BoxFuture<(), Error>; + /// Saves the target tunnel state and enters a blocking state. The state is restored + /// upon restart. + #[rpc(meta, name = "prepare_restart")] + fn prepare_restart(&self, Self::Metadata) -> BoxFuture<(), Error>; + /// Get previously used account tokens from the account history #[rpc(meta, name = "get_account_history")] fn get_account_history(&self, Self::Metadata) -> BoxFuture<Vec<AccountToken>, Error>; @@ -530,6 +535,11 @@ impl ManagementInterfaceApi for ManagementInterface { Box::new(self.send_command_to_daemon(DaemonCommand::Shutdown)) } + fn prepare_restart(&self, _: Self::Metadata) -> BoxFuture<(), Error> { + log::debug!("prepare_restart"); + Box::new(self.send_command_to_daemon(DaemonCommand::PrepareRestart)) + } + fn get_account_history(&self, _: Self::Metadata) -> BoxFuture<Vec<AccountToken>, Error> { log::debug!("get_account_history"); let (tx, rx) = sync::oneshot::channel(); diff --git a/mullvad-ipc-client/src/lib.rs b/mullvad-ipc-client/src/lib.rs index 631dbd054d..511f68ae3e 100644 --- a/mullvad-ipc-client/src/lib.rs +++ b/mullvad-ipc-client/src/lib.rs @@ -223,6 +223,10 @@ impl DaemonRpcClient { self.call("shutdown", &NO_ARGS) } + pub fn prepare_restart(&mut self) -> Result<()> { + self.call("prepare_restart", &NO_ARGS) + } + pub fn factory_reset(&mut self) -> Result<()> { self.call("factory_reset", &NO_ARGS) } diff --git a/mullvad-setup/Cargo.toml b/mullvad-setup/Cargo.toml new file mode 100644 index 0000000000..b467a7b174 --- /dev/null +++ b/mullvad-setup/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "mullvad-setup" +version = "2020.3.0" +authors = ["Mullvad VPN"] +description = "Tool used to manage daemon setup" +license = "GPL-3.0" +edition = "2018" +publish = false + +[[bin]] +name = "mullvad-setup" +path = "src/main.rs" + +[dependencies] +clap = "2.32" +env_logger = "0.7" +err-derive = "0.2.1" + +mullvad-ipc-client = { path = "../mullvad-ipc-client" } +mullvad-paths = { path = "../mullvad-paths" } +talpid-core = { path = "../talpid-core" } +talpid-types = { path = "../talpid-types" } + +[target.'cfg(windows)'.build-dependencies] +winres = "0.1" +winapi = "0.3" + +[package.metadata.winres] +ProductName = "Mullvad VPN" +CompanyName = "Mullvad VPN AB" +LegalCopyright = "(c) 2020 Mullvad VPN AB" +InternalName = "mullvad-setup" +OriginalFilename = "mullvad-setup.exe" diff --git a/mullvad-setup/build.rs b/mullvad-setup/build.rs new file mode 100644 index 0000000000..8cdd992c7c --- /dev/null +++ b/mullvad-setup/build.rs @@ -0,0 +1,18 @@ +use std::{env, fs, path::PathBuf}; + +fn main() { + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let product_version = env!("CARGO_PKG_VERSION").replacen(".0", "", 1); + fs::write(out_dir.join("product-version.txt"), &product_version).unwrap(); + + #[cfg(windows)] + { + let mut res = winres::WindowsResource::new(); + res.set("ProductVersion", &product_version); + res.set_language(winapi::um::winnt::MAKELANGID( + winapi::um::winnt::LANG_ENGLISH, + winapi::um::winnt::SUBLANG_ENGLISH_US, + )); + res.compile().expect("Unable to generate windows resources"); + } +} diff --git a/mullvad-setup/src/main.rs b/mullvad-setup/src/main.rs new file mode 100644 index 0000000000..f8fda169b5 --- /dev/null +++ b/mullvad-setup/src/main.rs @@ -0,0 +1,80 @@ +use clap::{crate_authors, crate_description, crate_name, SubCommand}; +use mullvad_ipc_client::{new_standalone_ipc_client, DaemonRpcClient}; +use std::{io, process}; +use talpid_core::firewall::{self, Firewall, FirewallArguments}; +use talpid_types::ErrorExt; + +pub const PRODUCT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/product-version.txt")); + +#[derive(err_derive::Error, Debug)] +pub enum Error { + #[error(display = "Failed to connect to daemon")] + DaemonConnect(#[error(source)] io::Error), + + #[error(display = "RPC call failed")] + DaemonRpcError(#[error(source)] mullvad_ipc_client::Error), + + #[error(display = "This command cannot be run if the daemon is active")] + DaemonIsRunning, + + #[error(display = "Firewall error")] + FirewallError(#[error(source)] firewall::Error), +} + +fn main() { + env_logger::init(); + + let subcommands = vec![ + SubCommand::with_name("prepare-restart") + .about("Move a running daemon into a blocking state and save its target state"), + SubCommand::with_name("reset-firewall") + .about("Remove any firewall rules introduced by the daemon"), + ]; + + let app = clap::App::new(crate_name!()) + .version(PRODUCT_VERSION) + .author(crate_authors!()) + .about(crate_description!()) + .setting(clap::AppSettings::SubcommandRequiredElseHelp) + .global_settings(&[ + clap::AppSettings::DisableHelpSubcommand, + clap::AppSettings::VersionlessSubcommands, + ]) + .subcommands(subcommands); + + let matches = app.get_matches(); + let result = match matches.subcommand_name().expect("Subcommand has no name") { + "prepare-restart" => prepare_restart(), + "reset-firewall" => reset_firewall(), + _ => unreachable!("No command matched"), + }; + + if let Err(e) = result { + eprintln!("{}", e.display_chain()); + process::exit(1); + } +} + +fn prepare_restart() -> Result<(), Error> { + let mut rpc = new_rpc_client()?; + rpc.prepare_restart().map_err(Error::DaemonRpcError) +} + +fn reset_firewall() -> Result<(), Error> { + // Ensure that the daemon isn't running + if let Ok(_) = new_rpc_client() { + return Err(Error::DaemonIsRunning); + } + + let mut firewall = Firewall::new(FirewallArguments { + initialize_blocked: false, + allow_lan: None, + }) + .map_err(Error::FirewallError)?; + + firewall.reset_policy().map_err(Error::FirewallError) +} + +fn new_rpc_client() -> Result<DaemonRpcClient, Error> { + new_standalone_ipc_client(&mullvad_paths::get_rpc_socket_path()).map_err(Error::DaemonConnect) +} diff --git a/version_metadata.sh b/version_metadata.sh index 55f5df631b..93344bba40 100755 --- a/version_metadata.sh +++ b/version_metadata.sh @@ -39,6 +39,7 @@ case "$1" in mullvad-daemon/Cargo.toml \ mullvad-cli/Cargo.toml \ mullvad-problem-report/Cargo.toml \ + mullvad-setup/Cargo.toml \ talpid-openvpn-plugin/Cargo.toml # Windows C++ @@ -67,6 +68,7 @@ EOF mv mullvad-daemon/Cargo.toml.bak mullvad-daemon/Cargo.toml || true mv mullvad-cli/Cargo.toml.bak mullvad-cli/Cargo.toml || true mv mullvad-problem-report/Cargo.toml.bak mullvad-problem-report/Cargo.toml || true + mv mullvad-setup/Cargo.toml.bak mullvad-setup/Cargo.toml || true mv talpid-openvpn-plugin/Cargo.toml.bak talpid-openvpn-plugin/Cargo.toml || true # Windows C++ mv dist-assets/windows/version.h.bak dist-assets/windows/version.h || true @@ -83,6 +85,7 @@ EOF rm mullvad-daemon/Cargo.toml.bak || true rm mullvad-cli/Cargo.toml.bak || true rm mullvad-problem-report/Cargo.toml.bak || true + rm mullvad-setup/Cargo.toml.bak || true rm talpid-openvpn-plugin/Cargo.toml.bak || true # Windows C++ rm dist-assets/windows/version.h.bak || true |
