summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOdd Stranne <odd@mullvad.net>2019-02-15 15:26:21 +0100
committerOdd Stranne <odd@mullvad.net>2019-02-15 15:26:21 +0100
commitdc4817c61bcc2b4be0ac20d2f864d70f05b2b294 (patch)
tree775e34b835ae48d0c39c0af6251944da1ccb1325
parentff2126b41d04f7e0810e993b0c7dcdd92f307dc4 (diff)
parentb36a2228d1833931bbfa729c21649c615f4e97e1 (diff)
downloadmullvadvpn-dc4817c61bcc2b4be0ac20d2f864d70f05b2b294.tar.xz
mullvadvpn-dc4817c61bcc2b4be0ac20d2f864d70f05b2b294.zip
Merge branch 'bundled-proxy-shadowsocks'
-rw-r--r--CHANGELOG.md2
-rw-r--r--Cargo.lock1
m---------dist-assets/binaries0
-rwxr-xr-xgui/packages/desktop/electron-builder.yml6
-rw-r--r--gui/packages/desktop/src/main/daemon-rpc.ts7
-rw-r--r--gui/packages/desktop/src/shared/daemon-rpc-types.ts8
-rw-r--r--mullvad-cli/src/cmds/tunnel.rs63
-rw-r--r--talpid-core/Cargo.toml1
-rw-r--r--talpid-core/src/lib.rs3
-rw-r--r--talpid-core/src/process/openvpn.rs24
-rw-r--r--talpid-core/src/proxy/mod.rs103
-rw-r--r--talpid-core/src/proxy/shadowsocks.rs261
-rw-r--r--talpid-core/src/tunnel/openvpn.rs135
-rw-r--r--talpid-types/src/net/openvpn.rs59
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(&params.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(&params.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(())
}