diff options
| author | Odd Stranne <odd@mullvad.net> | 2019-02-15 15:26:21 +0100 |
|---|---|---|
| committer | Odd Stranne <odd@mullvad.net> | 2019-02-15 15:26:21 +0100 |
| commit | dc4817c61bcc2b4be0ac20d2f864d70f05b2b294 (patch) | |
| tree | 775e34b835ae48d0c39c0af6251944da1ccb1325 | |
| parent | ff2126b41d04f7e0810e993b0c7dcdd92f307dc4 (diff) | |
| parent | b36a2228d1833931bbfa729c21649c615f4e97e1 (diff) | |
| download | mullvadvpn-dc4817c61bcc2b4be0ac20d2f864d70f05b2b294.tar.xz mullvadvpn-dc4817c61bcc2b4be0ac20d2f864d70f05b2b294.zip | |
Merge branch 'bundled-proxy-shadowsocks'
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | Cargo.lock | 1 | ||||
| m--------- | dist-assets/binaries | 0 | ||||
| -rwxr-xr-x | gui/packages/desktop/electron-builder.yml | 6 | ||||
| -rw-r--r-- | gui/packages/desktop/src/main/daemon-rpc.ts | 7 | ||||
| -rw-r--r-- | gui/packages/desktop/src/shared/daemon-rpc-types.ts | 8 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/tunnel.rs | 63 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 1 | ||||
| -rw-r--r-- | talpid-core/src/lib.rs | 3 | ||||
| -rw-r--r-- | talpid-core/src/process/openvpn.rs | 24 | ||||
| -rw-r--r-- | talpid-core/src/proxy/mod.rs | 103 | ||||
| -rw-r--r-- | talpid-core/src/proxy/shadowsocks.rs | 261 | ||||
| -rw-r--r-- | talpid-core/src/tunnel/openvpn.rs | 135 | ||||
| -rw-r--r-- | talpid-types/src/net/openvpn.rs | 59 |
14 files changed, 652 insertions, 21 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 44d0287573..07cd008847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ Line wrap the file at 100 chars. Th ## [Unreleased] +### Added +- Integrated Shadowsocks proxy support. Accessible via CLI. ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 1ddd1304de..95fb30478c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1972,6 +1972,7 @@ dependencies = [ "openvpn-plugin 0.3.0 (git+https://github.com/mullvad/openvpn-plugin-rs?branch=auth-failed-event)", "os_pipe 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "pfctl 0.2.1 (git+https://github.com/mullvad/pfctl-rs?rev=9f31b5ddcab941862470075eab83bb398195f3d6)", + "regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "resolv-conf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "rtnetlink 0.0.3 (git+https://github.com/mullvad/netlink?branch=ignore-hw-address)", "shell-escape 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/dist-assets/binaries b/dist-assets/binaries -Subproject c700c8b968415a0f398051c914dc7cc89317f87 +Subproject 271630da8e9b87665ea6ce10f151691e1055d6c diff --git a/gui/packages/desktop/electron-builder.yml b/gui/packages/desktop/electron-builder.yml index 0fb2f35639..fc05bc8d4b 100755 --- a/gui/packages/desktop/electron-builder.yml +++ b/gui/packages/desktop/electron-builder.yml @@ -51,6 +51,8 @@ mac: to: . - from: ../../../dist-assets/uninstall_macos.sh to: ./uninstall.sh + - from: ../../../dist-assets/binaries/macos/sslocal + to: . pkg: allowAnywhere: false @@ -93,6 +95,8 @@ win: to: . - from: ../../../dist-assets/binaries/windows/openvpn.exe to: . + - from: ../../../dist-assets/binaries/windows/sslocal.exe + to: . linux: target: @@ -113,6 +117,8 @@ linux: to: . - from: ../../../dist-assets/linux/mullvad-daemon.service to: . + - from: ../../../dist-assets/linux/sslocal + to: . deb: fpm: ["--before-install", "../../../dist-assets/linux/before-install.sh", diff --git a/gui/packages/desktop/src/main/daemon-rpc.ts b/gui/packages/desktop/src/main/daemon-rpc.ts index 390e301756..69ed725d52 100644 --- a/gui/packages/desktop/src/main/daemon-rpc.ts +++ b/gui/packages/desktop/src/main/daemon-rpc.ts @@ -162,6 +162,13 @@ const openVpnProxySchema = maybe( ), }), }), + object({ + shadowsocks: partialObject({ + peer: string, + password: string, + cipher: string, + }), + }), ), ); diff --git a/gui/packages/desktop/src/shared/daemon-rpc-types.ts b/gui/packages/desktop/src/shared/daemon-rpc-types.ts index bc6d6e048c..c4e2f6e53f 100644 --- a/gui/packages/desktop/src/shared/daemon-rpc-types.ts +++ b/gui/packages/desktop/src/shared/daemon-rpc-types.ts @@ -170,7 +170,7 @@ export interface ITunnelOptions { }; } -export type ProxySettings = ILocalProxySettings | IRemoteProxySettings; +export type ProxySettings = ILocalProxySettings | IRemoteProxySettings | IShadowsocksProxySettings; export interface ILocalProxySettings { port: number; @@ -187,6 +187,12 @@ export interface IRemoteProxyAuth { password: string; } +export interface IShadowsocksProxySettings { + peer: string; + password: string; + cipher: string; +} + export interface IAppVersionInfo { currentIsSupported: boolean; latest: { diff --git a/mullvad-cli/src/cmds/tunnel.rs b/mullvad-cli/src/cmds/tunnel.rs index 5887148abe..bbd1a0e2a1 100644 --- a/mullvad-cli/src/cmds/tunnel.rs +++ b/mullvad-cli/src/cmds/tunnel.rs @@ -2,7 +2,7 @@ use crate::{new_rpc_client, Command, Result}; use clap::value_t; use mullvad_types::settings::TunnelOptions; -use talpid_types::net::openvpn; +use talpid_types::net::openvpn::{self, SHADOWSOCKS_CIPHERS}; use std::net::{IpAddr, SocketAddr}; @@ -144,6 +144,35 @@ fn create_openvpn_proxy_subcommand() -> clap::App<'static, 'static> { .required(true) .index(4), ), + ) + .subcommand( + clap::SubCommand::with_name("shadowsocks") + .about("Configure bundled Shadowsocks proxy") + .arg( + clap::Arg::with_name("remote-ip") + .help("Specifies the IP of the remote Shadowsocks server") + .required(true) + .index(1), + ) + .arg( + clap::Arg::with_name("remote-port") + .help("Specifies the port of the remote Shadowsocks server") + .default_value("443") + .index(2), + ) + .arg( + clap::Arg::with_name("password") + .help("Specifies the password on the remote Shadowsocks server") + .default_value("23#dfsbbb") + .index(3), + ) + .arg( + clap::Arg::with_name("cipher") + .help("Specifies the cipher to use") + .default_value("chacha20") + .possible_values(SHADOWSOCKS_CIPHERS) + .index(4), + ), ), ) } @@ -301,6 +330,8 @@ impl Tunnel { Self::print_local_proxy(&local_proxy) } else if let openvpn::ProxySettings::Remote(remote_proxy) = proxy { Self::print_remote_proxy(&remote_proxy) + } else if let openvpn::ProxySettings::Shadowsocks(shadowsocks_proxy) = proxy { + Self::print_shadowsocks_proxy(&shadowsocks_proxy) } else { unreachable!("unhandled proxy type"); } @@ -330,6 +361,14 @@ impl Tunnel { } } + fn print_shadowsocks_proxy(proxy: &openvpn::ShadowsocksProxySettings) { + println!("proxy: Shadowsocks"); + println!(" peer IP: {}", proxy.peer.ip()); + println!(" peer port: {}", proxy.peer.port()); + println!(" password: {}", proxy.password); + println!(" cipher: {}", proxy.cipher); + } + fn process_openvpn_proxy_unset() -> Result<()> { let mut rpc = new_rpc_client()?; rpc.set_openvpn_proxy(None)?; @@ -388,6 +427,28 @@ impl Tunnel { let mut rpc = new_rpc_client()?; rpc.set_openvpn_proxy(Some(packed_proxy))?; + } else if let Some(args) = matches.subcommand_matches("shadowsocks") { + let remote_ip = + value_t!(args.value_of("remote-ip"), IpAddr).unwrap_or_else(|e| e.exit()); + let remote_port = + value_t!(args.value_of("remote-port"), u16).unwrap_or_else(|e| e.exit()); + let password = args.value_of("password").unwrap().to_string(); + let cipher = args.value_of("cipher").unwrap().to_string(); + + let proxy = openvpn::ShadowsocksProxySettings { + peer: SocketAddr::new(remote_ip, remote_port), + password, + cipher, + }; + + let packed_proxy = openvpn::ProxySettings::Shadowsocks(proxy); + + if let Err(error) = openvpn::ProxySettingsValidation::validate(&packed_proxy) { + panic!(error); + } + + let mut rpc = new_rpc_client()?; + rpc.set_openvpn_proxy(Some(packed_proxy))?; } else { unreachable!("unhandled proxy type"); } diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 4fcc06e61c..075cd0319f 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -24,6 +24,7 @@ uuid = { version = "0.6", features = ["v4"] } talpid-ipc = { path = "../talpid-ipc" } talpid-types = { path = "../talpid-types" } +regex = "1.1.0" [target.'cfg(unix)'.dependencies] hex = "0.3" diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs index 8e63d2bfa8..40f450aa72 100644 --- a/talpid-core/src/lib.rs +++ b/talpid-core/src/lib.rs @@ -51,6 +51,9 @@ pub mod dns; /// State machine to handle tunnel configuration. pub mod tunnel_state_machine; +/// Internal code for managing bundled proxy software. +mod proxy; + mod mktemp; /// Misc utilities for the Linux platform. diff --git a/talpid-core/src/process/openvpn.rs b/talpid-core/src/process/openvpn.rs index 50b9e59799..24d69d3926 100644 --- a/talpid-core/src/process/openvpn.rs +++ b/talpid-core/src/process/openvpn.rs @@ -64,6 +64,7 @@ pub struct OpenVpnCommand { tunnel_options: net::openvpn::TunnelOptions, tunnel_alias: Option<OsString>, enable_ipv6: bool, + proxy_port: Option<u16>, } impl OpenVpnCommand { @@ -84,6 +85,7 @@ impl OpenVpnCommand { tunnel_options: net::openvpn::TunnelOptions::default(), tunnel_alias: None, enable_ipv6: true, + proxy_port: None, } } @@ -162,6 +164,13 @@ impl OpenVpnCommand { self } + /// Sets the local proxy port bound to. + /// In case of dynamic port selection, this will only be known after the proxy has been started. + pub fn proxy_port(&mut self, proxy_port: u16) -> &mut Self { + self.proxy_port = Some(proxy_port); + self + } + /// Build a runnable expression from the current state of the command. pub fn build(&self) -> duct::Expression { log::debug!("Building expression: {}", &self); @@ -302,6 +311,21 @@ impl OpenVpnCommand { args.push("255.255.255.255".to_owned()); args.push("net_gateway".to_owned()); } + Some(net::openvpn::ProxySettings::Shadowsocks(ref ss)) => { + args.push("--socks-proxy".to_owned()); + args.push("127.0.0.1".to_owned()); + + if let Some(ref proxy_port) = self.proxy_port { + args.push(proxy_port.to_string()); + } else { + panic!("Dynamic proxy port was not registered with OpenVpnCommand"); + } + + args.push("--route".to_owned()); + args.push(ss.peer.ip().to_string()); + args.push("255.255.255.255".to_owned()); + args.push("net_gateway".to_owned()); + } None => {} }; args diff --git a/talpid-core/src/proxy/mod.rs b/talpid-core/src/proxy/mod.rs new file mode 100644 index 0000000000..dac4e63cd2 --- /dev/null +++ b/talpid-core/src/proxy/mod.rs @@ -0,0 +1,103 @@ +mod shadowsocks; + +pub use std::io::Result; + +use self::shadowsocks::ShadowsocksProxyMonitor; +use std::{fmt, path::PathBuf, sync::mpsc}; +use talpid_types::net::openvpn; + +pub enum WaitResult { + UnexpectedExit(String), + ProperShutdown, +} + +pub trait ProxyMonitor: Send { + /// Create a handle than can be used to ask the proxy service to shut down. + fn close_handle(&mut self) -> Box<dyn ProxyMonitorCloseHandle>; + + /// Consume monitor and wait for proxy service to shut down. + fn wait(self: Box<Self>) -> Result<WaitResult>; + + /// The port bound to. + fn port(&self) -> u16; +} + +impl fmt::Debug for ProxyMonitor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ProxyMonitor {{ port: {} }}", self.port()) + } +} + +pub trait ProxyMonitorCloseHandle: Send { + fn close(self: Box<Self>) -> Result<()>; +} + +struct NoopProxyMonitor { + tx: mpsc::Sender<()>, + rx: mpsc::Receiver<()>, + port: u16, +} + +impl NoopProxyMonitor { + fn start(port: u16) -> Result<Self> { + let (tx, rx) = mpsc::channel(); + Ok(NoopProxyMonitor { tx, rx, port }) + } +} + +impl ProxyMonitor for NoopProxyMonitor { + fn close_handle(&mut self) -> Box<dyn ProxyMonitorCloseHandle> { + Box::new(NoopProxyMonitorCloseHandle { + tx: self.tx.clone(), + }) + } + + fn wait(self: Box<Self>) -> Result<WaitResult> { + let _ = self.rx.recv(); + Ok(WaitResult::ProperShutdown) + } + + fn port(&self) -> u16 { + self.port + } +} + +struct NoopProxyMonitorCloseHandle { + tx: mpsc::Sender<()>, +} + +impl ProxyMonitorCloseHandle for NoopProxyMonitorCloseHandle { + fn close(self: Box<Self>) -> Result<()> { + let _ = self.tx.send(()); + Ok(()) + } +} + +/// Variables that define the environment to help +/// proxy implementations find their way around. +/// TODO: Move struct to wider scope and use more generic name. +pub struct ProxyResourceData { + pub resource_dir: PathBuf, + pub log_dir: Option<PathBuf>, +} + +pub fn start_proxy( + settings: &openvpn::ProxySettings, + resource_data: &ProxyResourceData, +) -> Result<Box<dyn ProxyMonitor>> { + match settings { + openvpn::ProxySettings::Local(local_settings) => { + // These are generic proxy settings with the proxy client not managed by us. + Ok(Box::new(NoopProxyMonitor::start(local_settings.port)?)) + } + openvpn::ProxySettings::Remote(remote_settings) => { + // These are generic proxy settings with the proxy client not managed by us. + Ok(Box::new(NoopProxyMonitor::start( + remote_settings.address.port(), + )?)) + } + openvpn::ProxySettings::Shadowsocks(ss_settings) => Ok(Box::new( + ShadowsocksProxyMonitor::start(ss_settings, resource_data)?, + )), + } +} diff --git a/talpid-core/src/proxy/shadowsocks.rs b/talpid-core/src/proxy/shadowsocks.rs new file mode 100644 index 0000000000..25bf041cdb --- /dev/null +++ b/talpid-core/src/proxy/shadowsocks.rs @@ -0,0 +1,261 @@ +pub use std::io::Result; + +use crate::logging; +use regex::Regex; + +use std::{ + borrow::Cow, + env, + ffi::OsString, + fmt, + fs::File, + io::{BufRead, Error, ErrorKind}, + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::PathBuf, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + thread, + time::Duration, +}; + +use super::{ProxyMonitor, ProxyMonitorCloseHandle, ProxyResourceData, WaitResult}; +use talpid_types::net::openvpn::ShadowsocksProxySettings; + +struct ShadowsocksCommand { + shadowsocks_bin: OsString, + local: Option<SocketAddr>, + peer: Option<SocketAddr>, + peer_password: Option<String>, + // This should map to the shadowsocks-rust `CipherType` type. + cipher: Option<String>, +} + +impl ShadowsocksCommand { + pub fn new(shadowsocks_bin: OsString) -> Self { + ShadowsocksCommand { + shadowsocks_bin, + local: None, + peer: None, + peer_password: None, + cipher: None, + } + } + + pub fn local(&mut self, local: SocketAddr) -> &mut Self { + self.local = Some(local); + self + } + + pub fn peer(&mut self, peer: SocketAddr) -> &mut Self { + self.peer = Some(peer); + self + } + + pub fn peer_password(&mut self, password: String) -> &mut Self { + self.peer_password = Some(password); + self + } + + pub fn cipher(&mut self, cipher: String) -> &mut Self { + self.cipher = Some(cipher); + self + } + + pub fn build(&self) -> duct::Expression { + log::debug!("Building expression: {}", &self); + duct::cmd(&self.shadowsocks_bin, self.get_arguments()).unchecked() + } + + fn get_arguments(&self) -> Vec<String> { + let mut args: Vec<String> = vec![]; + + // Always activate TCP no-delay. + args.push("--no-delay".to_owned()); + + if let Some(ref local) = self.local { + args.push("--local-addr".to_owned()); + args.push(format!("{}:{}", local.ip(), local.port())); + } + + if let Some(ref peer) = self.peer { + args.push("--server-addr".to_owned()); + args.push(format!("{}:{}", peer.ip(), peer.port())); + } + + if let Some(ref peer_password) = self.peer_password { + args.push("--password".to_owned()); + args.push(peer_password.to_owned()); + } + + if let Some(ref cipher) = self.cipher { + args.push("--encrypt-method".to_owned()); + args.push(cipher.to_string()); + } + + args + } +} + +impl fmt::Display for ShadowsocksCommand { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(&shell_escape::escape( + self.shadowsocks_bin.to_string_lossy(), + ))?; + for arg in &self.get_arguments() { + fmt.write_str(" ")?; + fmt.write_str(&shell_escape::escape(Cow::from(arg)))?; + } + Ok(()) + } +} + +pub struct ShadowsocksProxyMonitor { + subproc: Arc<duct::Handle>, + closed: Arc<AtomicBool>, + port: u16, +} + +const SHADOWSOCKS_LOG_FILENAME: &str = "shadowsocks.log"; +#[cfg(unix)] +const SHADOWSOCKS_BIN_FILENAME: &str = "sslocal"; +#[cfg(windows)] +const SHADOWSOCKS_BIN_FILENAME: &str = "sslocal.exe"; + +impl ShadowsocksProxyMonitor { + pub fn start( + settings: &ShadowsocksProxySettings, + resource_data: &ProxyResourceData, + ) -> Result<Self> { + let binary = resource_data + .resource_dir + .join(SHADOWSOCKS_BIN_FILENAME) + .into_os_string(); + + let mut cmd = ShadowsocksCommand::new(binary) + .local(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0)) + .peer(settings.peer) + .peer_password(settings.password.clone()) + .cipher(settings.cipher.clone()) + .build(); + + let log_dir: PathBuf = if let Some(ref log_dir) = resource_data.log_dir { + log_dir.clone() + } else { + env::temp_dir() + }; + + let logfile = log_dir.join(SHADOWSOCKS_LOG_FILENAME); + + logging::rotate_log(&logfile) + .map_err(|_| Error::new(ErrorKind::Other, "Failed to rotate log file"))?; + + cmd = cmd.stdin_null().stderr_to_stdout().stdout(&logfile); + + let subproc = cmd.start()?; + + match Self::get_bound_port(File::open(&logfile)?, &subproc) { + Ok(port) => Ok(Self { + subproc: Arc::new(subproc), + closed: Arc::new(AtomicBool::new(false)), + port, + }), + Err(err) => { + let _ = subproc.kill(); + Err(err) + } + } + } + + fn get_bound_port(logfile: File, subproc: &duct::Handle) -> Result<u16> { + let mut buffered_reader = std::io::BufReader::new(logfile); + + for _tries in 0..5 { + loop { + // `read_line` appends to the buffer so keep a small scope for the `line` variable. + let mut line = String::new(); + match buffered_reader.read_line(&mut line) { + Ok(bytes_read) => { + if bytes_read == 0 { + break; + } + // `read_line` includes the line break in the returned line. + if let Ok(port) = Self::parse_port(line.trim_end()) { + return Ok(port); + } + } + Err(_) => { + break; + } + } + } + if subproc.try_wait().unwrap().is_some() { + break; + } + thread::sleep(Duration::from_secs(1)); + } + + Err(Error::new( + ErrorKind::Other, + "Could not determine which port Shadowsocks has bound to", + )) + } + + fn parse_port(logline: &str) -> Result<u16> { + // TODO: Compile once and reuse. + let re = Regex::new(r"(?:TCP Listening on \d+\.\d+\.\d+\.\d+:)(\d+$)").unwrap(); + + if let Some(captures) = re.captures(logline) { + return Ok(captures[1] + .parse() + .map_err(|_| Error::new(ErrorKind::Other, "Failed to parse port number string"))?); + } + + return Err(Error::new(ErrorKind::Other, "No port number present")); + } +} + +impl ProxyMonitor for ShadowsocksProxyMonitor { + fn close_handle(&mut self) -> Box<dyn ProxyMonitorCloseHandle> { + Box::new(ShadowsocksProxyMonitorCloseHandle { + subproc: self.subproc.clone(), + closed: self.closed.clone(), + }) + } + + fn wait(self: Box<Self>) -> Result<WaitResult> { + self.subproc.wait().map(|output| { + if self.closed.load(Ordering::SeqCst) { + Ok(WaitResult::ProperShutdown) + } else { + Ok(WaitResult::UnexpectedExit( + if let Some(exit_code) = output.status.code() { + format!("Exit code: {}", exit_code) + } else { + "Exit code is indeterminable".to_string() + }, + )) + } + })? + } + + fn port(&self) -> u16 { + self.port + } +} + +pub struct ShadowsocksProxyMonitorCloseHandle { + subproc: Arc<duct::Handle>, + closed: Arc<AtomicBool>, +} + +impl ProxyMonitorCloseHandle for ShadowsocksProxyMonitorCloseHandle { + fn close(self: Box<Self>) -> Result<()> { + if !self.closed.swap(true, Ordering::SeqCst) { + self.subproc.kill() + } else { + Ok(()) + } + } +} diff --git a/talpid-core/src/tunnel/openvpn.rs b/talpid-core/src/tunnel/openvpn.rs index 38aee11755..7af5f6c63e 100644 --- a/talpid-core/src/tunnel/openvpn.rs +++ b/talpid-core/src/tunnel/openvpn.rs @@ -5,6 +5,7 @@ use crate::{ openvpn::{OpenVpnCommand, OpenVpnProcHandle}, stoppable_process::StoppableProcess, }, + proxy::{self, ProxyMonitor, ProxyResourceData}, }; #[cfg(target_os = "linux")] use failure::ResultExt as FailureResultExt; @@ -67,6 +68,11 @@ error_chain! { CredentialsWriteError { description("Error while writing credentials to temporary file") } + /// Failures related to the proxy service. + ProxyError(msg: String) { + description("Unable to start, wait for or kill the proxy service") + display("Proxy error: {}", msg) + } } } @@ -93,6 +99,7 @@ const OPENVPN_BIN_FILENAME: &str = "openvpn.exe"; #[derive(Debug)] pub struct OpenVpnMonitor<C: OpenVpnBuilder = OpenVpnCommand> { child: Arc<C::ProcessHandle>, + proxy_monitor: Option<Box<dyn ProxyMonitor>>, event_dispatcher: Option<talpid_ipc::IpcServer>, log_path: Option<PathBuf>, closed: Arc<AtomicBool>, @@ -122,7 +129,6 @@ impl OpenVpnMonitor<OpenVpnCommand> { let proxy_auth_file = Self::create_proxy_auth_file(¶ms.options.proxy) .chain_err(|| ErrorKind::CredentialsWriteError)?; - let user_pass_file_path = user_pass_file.to_path_buf(); let proxy_auth_file_path = match proxy_auth_file { @@ -145,6 +151,20 @@ impl OpenVpnMonitor<OpenVpnCommand> { None => log::debug!("Ignoring OpenVpnEvent {:?}", event), } }; + + let log_dir: Option<PathBuf> = if let Some(ref log_path) = log_path { + Some(log_path.parent().unwrap().into()) + } else { + None + }; + + let proxy_resources = proxy::ProxyResourceData { + resource_dir: resource_dir.to_path_buf(), + log_dir, + }; + + let proxy_monitor = Self::start_proxy(¶ms.options.proxy, &proxy_resources)?; + let cmd = Self::create_openvpn_cmd( params, tunnel_alias, @@ -154,6 +174,7 @@ impl OpenVpnMonitor<OpenVpnCommand> { _ => None, }, resource_dir, + &proxy_monitor, )?; let plugin_path = Self::get_plugin_path(resource_dir)?; @@ -165,11 +186,12 @@ impl OpenVpnMonitor<OpenVpnCommand> { log_path, user_pass_file, proxy_auth_file, + proxy_monitor, ) } } -impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { +impl<C: OpenVpnBuilder + 'static> OpenVpnMonitor<C> { fn new_internal<L>( mut cmd: C, on_event: L, @@ -177,6 +199,7 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { log_path: Option<PathBuf>, user_pass_file: mktemp::TempFile, proxy_auth_file: Option<mktemp::TempFile>, + proxy_monitor: Option<Box<dyn ProxyMonitor>>, ) -> Result<OpenVpnMonitor<C>> where L: Fn(openvpn_plugin::EventType, HashMap<String, String>) + Send + Sync + 'static, @@ -184,16 +207,15 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { let event_dispatcher = event_server::start(on_event).chain_err(|| ErrorKind::EventDispatcherError)?; - let child = cmd .plugin(plugin_path, vec![event_dispatcher.path().to_owned()]) .log(log_path.as_ref().map(|p| p.as_path())) .start() .chain_err(|| ErrorKind::ChildProcessError("Failed to start"))?; - Ok(OpenVpnMonitor { child: Arc::new(child), + proxy_monitor, event_dispatcher: Some(event_dispatcher), log_path, closed: Arc::new(AtomicBool::new(false)), @@ -203,8 +225,7 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { } /// Creates a handle to this monitor, allowing the tunnel to be closed while some other - /// thread - /// is blocked in `wait`. + /// thread is blocked in `wait`. pub fn close_handle(&self) -> OpenVpnCloseHandle<C::ProcessHandle> { OpenVpnCloseHandle { child: self.child.clone(), @@ -212,11 +233,68 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { } } - /// Consumes the monitor and blocks until OpenVPN exits or there is an error in either - /// waiting - /// for the process or in the event dispatcher. + /// Consumes the monitor and waits for both proxy and tunnel, as applicable. pub fn wait(mut self) -> Result<()> { - match self.wait_result() { + if let Some(mut proxy_monitor) = self.proxy_monitor.take() { + let (tx_tunnel, rx) = mpsc::channel(); + let tx_proxy = tx_tunnel.clone(); + let tunnel_close_handle = self.close_handle(); + let proxy_close_handle = proxy_monitor.close_handle(); + + enum Stopped { + Tunnel(Result<()>), + Proxy(proxy::Result<proxy::WaitResult>), + } + + thread::spawn(move || { + tx_tunnel.send(Stopped::Tunnel(self.wait_tunnel())).unwrap(); + let _ = proxy_close_handle.close(); + }); + + thread::spawn(move || { + tx_proxy.send(Stopped::Proxy(proxy_monitor.wait())).unwrap(); + let _ = tunnel_close_handle.close(); + }); + + let result = rx.recv().unwrap(); + let _ = rx.recv(); + + match result { + Stopped::Tunnel(tunnel_result) => { + return tunnel_result; + } + Stopped::Proxy(proxy_result) => { + // The proxy should never exit before openvpn. + match proxy_result { + Ok(proxy::WaitResult::ProperShutdown) => { + return Err(ErrorKind::ProxyError("The proxy exited unexpectedly without providing additional details".into()).into()); + } + Ok(proxy::WaitResult::UnexpectedExit(details)) => { + return Err(ErrorKind::ProxyError(format!( + "The proxy exited unexpectedly providing these details: {}", + details + )) + .into()); + } + Err(err) => { + return Err(err).chain_err(|| { + ErrorKind::ProxyError( + "Failed to wait for/monitor proxy service".into(), + ) + }); + } + } + } + } + } + // No proxy active, wait only for the tunnel. + self.wait_tunnel() + } + + /// Supplement `inner_wait_tunnel()` with logging and error handling. + fn wait_tunnel(&mut self) -> Result<()> { + let result = self.inner_wait_tunnel(); + match result { WaitResult::Child(Ok(exit_status), closed) => { if exit_status.success() || closed { log::debug!( @@ -242,7 +320,7 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { /// Waits for both the child process and the event dispatcher in parallel. After both have /// returned this returns the earliest result. - fn wait_result(&mut self) -> WaitResult { + fn inner_wait_tunnel(&mut self) -> WaitResult { let child_wait_handle = self.child.clone(); let closed_handle = self.closed.clone(); let child_close_handle = self.close_handle(); @@ -270,12 +348,12 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { } /// Performs a postmortem analysis to attempt to provide a more detailed error result. - fn postmortem(self) -> Error { + fn postmortem(&mut self) -> Error { #[cfg(windows)] { use std::fs; - if let Some(log_path) = self.log_path { + if let Some(log_path) = self.log_path.take() { if let Ok(log) = fs::read_to_string(log_path) { if log.contains("There are no TAP-Windows adapters on this system") { return ErrorKind::MissingTapAdapter.into(); @@ -291,9 +369,9 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { } fn create_proxy_auth_file( - proxy: &Option<openvpn::ProxySettings>, + proxy_settings: &Option<openvpn::ProxySettings>, ) -> ::std::result::Result<Option<mktemp::TempFile>, io::Error> { - if let Some(openvpn::ProxySettings::Remote(ref remote_proxy)) = proxy { + if let Some(openvpn::ProxySettings::Remote(ref remote_proxy)) = proxy_settings { if let Some(ref proxy_auth) = remote_proxy.auth { return Ok(Some(Self::create_credentials_file( &proxy_auth.username, @@ -304,6 +382,19 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { Ok(None) } + /// Starts a proxy service, as applicable. + fn start_proxy( + proxy_settings: &Option<openvpn::ProxySettings>, + proxy_resources: &ProxyResourceData, + ) -> Result<Option<Box<dyn ProxyMonitor>>> { + if let Some(ref settings) = proxy_settings { + let proxy_monitor = proxy::start_proxy(settings, proxy_resources) + .chain_err(|| ErrorKind::ProxyError("Failed to start proxy service".into()))?; + return Ok(Some(proxy_monitor)); + } + Ok(None) + } + fn create_credentials_file(username: &str, password: &str) -> io::Result<mktemp::TempFile> { let temp_file = mktemp::TempFile::new(); log::debug!("Writing credentials to {}", temp_file.as_ref().display()); @@ -342,6 +433,7 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { user_pass_file: &Path, proxy_auth_file: Option<&Path>, resource_dir: &Path, + proxy_monitor: &Option<Box<dyn ProxyMonitor>>, ) -> Result<OpenVpnCommand> { let mut cmd = OpenVpnCommand::new(Self::get_openvpn_bin(resource_dir)?); if let Some(config) = Self::get_config_path(resource_dir) { @@ -362,6 +454,9 @@ impl<C: OpenVpnBuilder> OpenVpnMonitor<C> { if let Some(proxy_auth_file) = proxy_auth_file { cmd.proxy_auth(proxy_auth_file); } + if let Some(proxy) = proxy_monitor { + cmd.proxy_port(proxy.port()); + } Ok(cmd) } @@ -587,6 +682,7 @@ mod tests { None, TempFile::new(), None, + None, ); assert_eq!( Some(PathBuf::from("./my_test_plugin")), @@ -604,6 +700,7 @@ mod tests { Some(PathBuf::from("./my_test_log_file")), TempFile::new(), None, + None, ); assert_eq!( Some(PathBuf::from("./my_test_log_file")), @@ -616,7 +713,7 @@ mod tests { let mut builder = TestOpenVpnBuilder::default(); builder.process_handle = Some(TestProcessHandle(0)); let testee = - OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None, TempFile::new(), None) + OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None, TempFile::new(), None, None) .unwrap(); assert!(testee.wait().is_ok()); } @@ -626,7 +723,7 @@ mod tests { let mut builder = TestOpenVpnBuilder::default(); builder.process_handle = Some(TestProcessHandle(1)); let testee = - OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None, TempFile::new(), None) + OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None, TempFile::new(), None, None) .unwrap(); assert!(testee.wait().is_err()); } @@ -636,7 +733,7 @@ mod tests { let mut builder = TestOpenVpnBuilder::default(); builder.process_handle = Some(TestProcessHandle(1)); let testee = - OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None, TempFile::new(), None) + OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None, TempFile::new(), None, None) .unwrap(); testee.close_handle().close().unwrap(); assert!(testee.wait().is_ok()); @@ -646,7 +743,7 @@ mod tests { fn failed_process_start() { let builder = TestOpenVpnBuilder::default(); let error = - OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None, TempFile::new(), None) + OpenVpnMonitor::new_internal(builder, |_, _| {}, "", None, TempFile::new(), None, None) .unwrap_err(); match error.kind() { ErrorKind::ChildProcessError(_) => (), diff --git a/talpid-types/src/net/openvpn.rs b/talpid-types/src/net/openvpn.rs index 2f8c92f65e..1bc774a8d6 100644 --- a/talpid-types/src/net/openvpn.rs +++ b/talpid-types/src/net/openvpn.rs @@ -47,8 +47,12 @@ pub struct TunnelOptions { #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ProxySettings { + /// Generic proxy running independently on localhost. Local(LocalProxySettings), + /// Generic proxy running on remote host. Remote(RemoteProxySettings), + /// Bundled Shadowsocks proxy. + Shadowsocks(ShadowsocksProxySettings), } impl ProxySettings { @@ -56,6 +60,7 @@ impl ProxySettings { match self { ProxySettings::Local(settings) => settings.get_endpoint(), ProxySettings::Remote(settings) => settings.get_endpoint(), + ProxySettings::Shadowsocks(settings) => settings.get_endpoint(), } } } @@ -96,6 +101,47 @@ pub struct ProxyAuth { pub password: String, } +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub struct ShadowsocksProxySettings { + pub peer: SocketAddr, + /// Password on peer. + pub password: String, + pub cipher: String, +} + +pub static SHADOWSOCKS_CIPHERS: &[&str] = &[ + // Stream ciphers. + "aes-128-cfb", + "aes-128-cfb1", + "aes-128-cfb8", + "aes-128-cfb128", + "aes-256-cfb", + "aes-256-cfb1", + "aes-256-cfb8", + "aes-256-cfb128", + "rc4", + "rc4-md5", + "chacha20", + "salsa20", + "chacha20-ietf", + // AEAD ciphers. + "aes-128-gcm", + "aes-256-gcm", + "chacha20-ietf-poly1305", + "xchacha20-ietf-poly1305", + "aes-128-pmac-siv", + "aes-256-pmac-siv", +]; + +impl ShadowsocksProxySettings { + pub fn get_endpoint(&self) -> Endpoint { + Endpoint { + address: self.peer, + protocol: TransportProtocol::Tcp, + } + } +} + pub struct ProxySettingsValidation; impl ProxySettingsValidation { @@ -122,6 +168,19 @@ impl ProxySettingsValidation { return Err(String::from("localhost is not a valid remote server")); } } + ProxySettings::Shadowsocks(ss) => { + if ss.peer.ip().is_loopback() { + return Err(String::from( + "localhost is not a valid peer in this context", + )); + } + if ss.peer.port() == 0 { + return Err(String::from("Invalid remote port number")); + } + if !SHADOWSOCKS_CIPHERS.contains(&ss.cipher.as_str()) { + return Err(String::from("Invalid cipher")); + } + } }; Ok(()) } |
