summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOdd Stranne <odd@mullvad.net>2019-02-05 22:51:11 +0100
committerOdd Stranne <odd@mullvad.net>2019-02-15 15:06:51 +0100
commitaab22540c2a8e275a5d263d0c0b54237ec46d5b4 (patch)
treeb73640c0db76a73c5a04796b2c0c8e390d9ced22
parent58411be7cccf213d6ea5998ed84ce18740825258 (diff)
downloadmullvadvpn-aab22540c2a8e275a5d263d0c0b54237ec46d5b4.tar.xz
mullvadvpn-aab22540c2a8e275a5d263d0c0b54237ec46d5b4.zip
Add proxy module and Shadowsocks handler
-rw-r--r--Cargo.lock1
-rw-r--r--talpid-core/Cargo.toml1
-rw-r--r--talpid-core/src/lib.rs3
-rw-r--r--talpid-core/src/proxy/mod.rs103
-rw-r--r--talpid-core/src/proxy/shadowsocks.rs261
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(())
+ }
+ }
+}