diff options
| author | Odd Stranne <odd@mullvad.net> | 2019-02-05 22:51:11 +0100 |
|---|---|---|
| committer | Odd Stranne <odd@mullvad.net> | 2019-02-15 15:06:51 +0100 |
| commit | aab22540c2a8e275a5d263d0c0b54237ec46d5b4 (patch) | |
| tree | b73640c0db76a73c5a04796b2c0c8e390d9ced22 | |
| parent | 58411be7cccf213d6ea5998ed84ce18740825258 (diff) | |
| download | mullvadvpn-aab22540c2a8e275a5d263d0c0b54237ec46d5b4.tar.xz mullvadvpn-aab22540c2a8e275a5d263d0c0b54237ec46d5b4.zip | |
Add proxy module and Shadowsocks handler
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 1 | ||||
| -rw-r--r-- | talpid-core/src/lib.rs | 3 | ||||
| -rw-r--r-- | talpid-core/src/proxy/mod.rs | 103 | ||||
| -rw-r--r-- | talpid-core/src/proxy/shadowsocks.rs | 261 |
5 files changed, 369 insertions, 0 deletions
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/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/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(()) + } + } +} |
