summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2020-11-16 13:21:27 +0100
committerDavid Lönnhager <david.l@mullvad.net>2020-11-16 13:21:27 +0100
commit198c93cfdd45ec3940cf898714791b22986ce248 (patch)
treec29138ae5b1a90118029900ed2537bdc3fe76ab7
parent3b69022ce136824985f6ffe13d23908de3d610dc (diff)
parentcf4b8e19376d353ceb2132edbeefb52224c2b74a (diff)
downloadmullvadvpn-198c93cfdd45ec3940cf898714791b22986ce248.tar.xz
mullvadvpn-198c93cfdd45ec3940cf898714791b22986ce248.zip
Merge remote-tracking branch 'origin/route-monitor-openvpn'
-rw-r--r--CHANGELOG.md1
-rw-r--r--talpid-core/src/dns/linux/network_manager.rs17
-rw-r--r--talpid-core/src/process/openvpn.rs3
-rw-r--r--talpid-core/src/tunnel/mod.rs23
-rw-r--r--talpid-core/src/tunnel/openvpn.rs231
5 files changed, 243 insertions, 32 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b960371ce..09f3a72964 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,6 +55,7 @@ Line wrap the file at 100 chars. Th
#### Linux
- Make route monitor ignore loopback routes.
- Increase NetworkManager device readiness timeout to 15 seconds.
+- Set up routes for OpenVPN using the route manager instead of relying on OpenVPN.
### Fixed
- Fix missing map animation after selecting a new location in the desktop app.
diff --git a/talpid-core/src/dns/linux/network_manager.rs b/talpid-core/src/dns/linux/network_manager.rs
index 1e170bed11..d127f22085 100644
--- a/talpid-core/src/dns/linux/network_manager.rs
+++ b/talpid-core/src/dns/linux/network_manager.rs
@@ -172,12 +172,6 @@ impl NetworkManager {
.get(NM_DEVICE, "Ip4Config")
.map_err(Error::Dbus)?;
- let device_routes: Vec<Vec<u32>> = self
- .dbus_connection
- .with_path(NM_BUS, &device_ip4_config, RPC_TIMEOUT_MS)
- .get(NM_IP4_CONFIG, "Routes")
- .map_err(Error::Dbus)?;
-
let device_route_data: Vec<HashMap<String, Variant<Box<dyn RefArg>>>> = self
.dbus_connection
.with_path(NM_BUS, &device_ip4_config, RPC_TIMEOUT_MS)
@@ -185,8 +179,8 @@ impl NetworkManager {
.map_err(Error::Dbus)?;
ipv4_settings.insert("route-metric", Variant(Box::new(0u32)));
- ipv4_settings.insert("routes", Variant(Box::new(device_routes)));
ipv4_settings.insert("route-data", Variant(Box::new(device_route_data)));
+ ipv4_settings.remove("routes");
}
if let Some(ipv6_settings) = settings.get_mut("ipv6") {
@@ -201,13 +195,6 @@ impl NetworkManager {
.with_path(NM_BUS, &device_ip6_config, RPC_TIMEOUT_MS)
.get(NM_IP6_CONFIG, "Addresses")
.map_err(Error::Dbus)?;
-
- let device_routes6: Vec<(Vec<u8>, u32, Vec<u8>, u32)> = self
- .dbus_connection
- .with_path(NM_BUS, &device_ip6_config, RPC_TIMEOUT_MS)
- .get(NM_IP6_CONFIG, "Routes")
- .map_err(Error::Dbus)?;
-
let device_route6_data: Vec<HashMap<String, Variant<Box<dyn RefArg>>>> = self
.dbus_connection
.with_path(NM_BUS, &device_ip6_config, RPC_TIMEOUT_MS)
@@ -215,7 +202,7 @@ impl NetworkManager {
.map_err(Error::Dbus)?;
ipv6_settings.insert("route-metric", Variant(Box::new(0u32)));
- ipv6_settings.insert("routes", Variant(Box::new(device_routes6)));
+ ipv6_settings.remove("routes");
ipv6_settings.insert("route-data", Variant(Box::new(device_route6_data)));
// if the link contains link local addresses, addresses shouldn't be reset
if ipv6_settings
diff --git a/talpid-core/src/process/openvpn.rs b/talpid-core/src/process/openvpn.rs
index 7922ba952c..e4172ff4ec 100644
--- a/talpid-core/src/process/openvpn.rs
+++ b/talpid-core/src/process/openvpn.rs
@@ -42,6 +42,9 @@ static BASE_ARGUMENTS: &[&[&str]] = &[
"vpn_gateway",
"1",
],
+ // The route manager is used to add the routes.
+ #[cfg(target_os = "linux")]
+ &["--route-noexec"],
];
static ALLOWED_TLS1_2_CIPHERS: &[&str] = &[
diff --git a/talpid-core/src/tunnel/mod.rs b/talpid-core/src/tunnel/mod.rs
index 6f7e110102..81ca04519a 100644
--- a/talpid-core/src/tunnel/mod.rs
+++ b/talpid-core/src/tunnel/mod.rs
@@ -160,14 +160,9 @@ impl TunnelMonitor {
match tunnel_parameters {
#[cfg(not(target_os = "android"))]
- TunnelParameters::OpenVpn(config) => Self::start_openvpn_tunnel(
- &config,
- log_file,
- resource_dir,
- on_event,
- #[cfg(target_os = "linux")]
- route_manager,
- ),
+ TunnelParameters::OpenVpn(config) => {
+ Self::start_openvpn_tunnel(&config, log_file, resource_dir, on_event, route_manager)
+ }
#[cfg(target_os = "android")]
TunnelParameters::OpenVpn(_) => Err(Error::UnsupportedPlatform),
@@ -230,19 +225,13 @@ impl TunnelMonitor {
log: Option<PathBuf>,
resource_dir: &Path,
on_event: L,
- #[cfg(target_os = "linux")] route_manager: &mut RouteManager,
+ route_manager: &mut RouteManager,
) -> Result<Self>
where
L: Fn(TunnelEvent) + Send + Sync + 'static,
{
- let monitor = openvpn::OpenVpnMonitor::start(
- on_event,
- config,
- log,
- resource_dir,
- #[cfg(target_os = "linux")]
- route_manager,
- )?;
+ let monitor =
+ openvpn::OpenVpnMonitor::start(on_event, config, log, resource_dir, route_manager)?;
Ok(TunnelMonitor {
monitor: InternalTunnelMonitor::OpenVpn(monitor),
})
diff --git a/talpid-core/src/tunnel/openvpn.rs b/talpid-core/src/tunnel/openvpn.rs
index f83cabca3b..a327628037 100644
--- a/talpid-core/src/tunnel/openvpn.rs
+++ b/talpid-core/src/tunnel/openvpn.rs
@@ -1,4 +1,6 @@
use super::TunnelEvent;
+#[cfg(target_os = "linux")]
+use crate::routing::RequiredRoute;
use crate::{
mktemp,
process::{
@@ -8,6 +10,10 @@ use crate::{
proxy::{self, ProxyMonitor, ProxyResourceData},
routing,
};
+#[cfg(target_os = "linux")]
+use ipnetwork::IpNetwork;
+use lazy_static::lazy_static;
+use regex::Regex;
use std::{
collections::HashMap,
fs,
@@ -21,12 +27,21 @@ use std::{
thread,
time::Duration,
};
+#[cfg(target_os = "linux")]
+use std::{collections::HashSet, net::IpAddr};
use talpid_types::net::openvpn;
+#[cfg(target_os = "linux")]
+use talpid_types::ErrorExt;
use tokio::task;
#[cfg(target_os = "linux")]
use which;
+lazy_static! {
+ static ref ENV_ROUTE_ENTRY: Regex = Regex::new(r"route_(ipv6_)?(\w+)_(\d+)").unwrap();
+}
+
+
/// Results from fallible operations on the OpenVPN tunnel.
pub type Result<T> = std::result::Result<T, Error>;
@@ -39,6 +54,7 @@ pub enum Error {
RuntimeError(#[error(source)] io::Error),
/// Failed to set up routing.
+ #[cfg(target_os = "linux")]
#[error(display = "Failed to setup routing")]
SetupRoutingError(#[error(source)] routing::Error),
@@ -104,6 +120,26 @@ pub enum Error {
#[cfg(windows)]
#[error(display = "Failure in Windows syscall")]
WinnetError(#[error(source)] crate::winnet::Error),
+
+ /// Error routes from the provided map
+ #[cfg(target_os = "linux")]
+ #[error(display = "Failed to parse OpenVPN-provided routes")]
+ ParseRouteError(#[error(source)] RouteParseError),
+
+ /// The map is missing 'dev'
+ #[cfg(target_os = "linux")]
+ #[error(display = "Failed to obtain tunnel interface name")]
+ MissingTunnelInterface,
+
+ /// The map has no 'route_n' entries
+ #[cfg(target_os = "linux")]
+ #[error(display = "Failed to obtain OpenVPN server")]
+ MissingRemoteHost,
+
+ /// Cannot parse the remote_n in the provided map
+ #[cfg(target_os = "linux")]
+ #[error(display = "Cannot parse remote host string")]
+ ParseRemoteHost(#[error(source)] std::net::AddrParseError),
}
@@ -142,6 +178,7 @@ pub struct OpenVpnMonitor<C: OpenVpnBuilder = OpenVpnCommand> {
server_join_handle: Option<task::JoinHandle<std::result::Result<(), event_server::Error>>>,
}
+
impl OpenVpnMonitor<OpenVpnCommand> {
/// Creates a new `OpenVpnMonitor` with the given listener and using the plugin at the given
/// path.
@@ -151,6 +188,7 @@ impl OpenVpnMonitor<OpenVpnCommand> {
log_path: Option<PathBuf>,
resource_dir: &Path,
#[cfg(target_os = "linux")] route_manager: &mut routing::RouteManager,
+ #[cfg(not(target_os = "linux"))] _route_manager: &mut routing::RouteManager,
) -> Result<Self>
where
L: Fn(TunnelEvent) + Send + Sync + 'static,
@@ -181,6 +219,11 @@ impl OpenVpnMonitor<OpenVpnCommand> {
.clone()
.set_tunnel_link(interface)
.unwrap();
+ let routes = extract_routes(&env).unwrap();
+ if let Err(error) = route_manager_handle.clone().add_routes(routes) {
+ log::error!("{}", error.display_chain());
+ panic!("Failed to add routes");
+ }
});
return;
}
@@ -237,6 +280,194 @@ impl OpenVpnMonitor<OpenVpnCommand> {
}
}
+#[cfg(target_os = "linux")]
+#[derive(Debug)]
+struct OpenVpnRoute {
+ network: IpNetwork,
+ gateway: IpAddr,
+}
+
+#[cfg(target_os = "linux")]
+#[derive(err_derive::Error, Debug)]
+#[error(no_from)]
+#[allow(missing_docs)]
+pub enum RouteParseError {
+ #[error(display = "The route contains no network")]
+ MissingNetwork,
+ #[error(display = "The route contains no gateway")]
+ MissingGateway,
+ #[error(display = "Failed to parse route network address")]
+ ParseNetworkAddress(#[error(source)] std::net::AddrParseError),
+ #[error(display = "Failed to parse route network")]
+ ParseNetwork(#[error(source)] ipnetwork::IpNetworkError),
+ #[error(display = "Failed to parse route mask address")]
+ ParseMaskAddress(#[error(source)] std::net::AddrParseError),
+ #[error(display = "Failed to convert route mask to prefix")]
+ ParseMask(#[error(source)] ipnetwork::IpNetworkError),
+ #[error(display = "Failed to parse route gateway address")]
+ ParseGatewayAddress(#[error(source)] std::net::AddrParseError),
+}
+
+#[cfg(target_os = "linux")]
+fn parse_openvpn_dict_routes(
+ env: &HashMap<String, String>,
+) -> std::result::Result<Vec<OpenVpnRoute>, RouteParseError> {
+ let mut routes4 = HashMap::<u32, HashMap<&str, &str>>::new();
+ let mut routes6 = HashMap::new();
+
+ const NUM_EXPECTED_CAPTURES: usize = 4;
+
+ for (key, value) in env.iter() {
+ if let Some(captures) = ENV_ROUTE_ENTRY.captures(key) {
+ if captures.len() != NUM_EXPECTED_CAPTURES {
+ log::error!("Bug: unexpected number of captures");
+ continue;
+ }
+
+ let route_index: u32 = captures[3].parse().unwrap();
+ let property = captures.get(2).unwrap().as_str();
+
+ let is_ipv6 = captures.get(1).is_some();
+ let properties = if is_ipv6 {
+ routes6.entry(route_index).or_default()
+ } else {
+ routes4.entry(route_index).or_default()
+ };
+ (*properties).insert(property, value);
+ }
+ }
+
+ let parse_dict = |routes: HashMap<u32, HashMap<&str, &str>>,
+ is_ipv4|
+ -> std::result::Result<Vec<OpenVpnRoute>, RouteParseError> {
+ let mut routes = routes.into_iter().collect::<Vec<_>>();
+ routes.sort_by(|(idx0, _), (idx1, _)| idx0.cmp(idx1));
+
+ let mut parsed_list = Vec::with_capacity(routes.len());
+
+ for (_, route) in routes {
+ let network = route
+ .get("network")
+ .ok_or(RouteParseError::MissingNetwork)?;
+
+ let network = if is_ipv4 {
+ let network = network
+ .parse()
+ .map_err(RouteParseError::ParseNetworkAddress)?;
+
+ let mask: IpAddr = if let Some(mask) = route.get("netmask") {
+ mask.parse().map_err(RouteParseError::ParseMaskAddress)?
+ } else {
+ "255.255.255.255".parse().unwrap()
+ };
+
+ IpNetwork::with_netmask(network, mask).map_err(RouteParseError::ParseNetwork)?
+ } else {
+ network.parse().map_err(RouteParseError::ParseNetwork)?
+ };
+
+ let gateway = route
+ .get("gateway")
+ .ok_or(RouteParseError::MissingGateway)?
+ .parse()
+ .map_err(RouteParseError::ParseGatewayAddress)?;
+
+ parsed_list.push(OpenVpnRoute { network, gateway });
+ }
+ Ok(parsed_list)
+ };
+
+ let mut routes = parse_dict(routes4, true)?;
+ routes.extend(parse_dict(routes6, false)?);
+
+ Ok(routes)
+}
+
+#[cfg(target_os = "linux")]
+fn extract_routes(env: &HashMap<String, String>) -> Result<HashSet<RequiredRoute>> {
+ let mut routes = HashSet::new();
+
+ let default_node_ip: IpAddr = env
+ .get("route_net_gateway")
+ .ok_or(Error::ParseRouteError(RouteParseError::MissingGateway))?
+ .parse()
+ .map_err(RouteParseError::ParseGatewayAddress)
+ .map_err(Error::ParseRouteError)?;
+ let tun_gateway_ip: IpAddr = env
+ .get("route_vpn_gateway")
+ .ok_or(Error::ParseRouteError(RouteParseError::MissingGateway))?
+ .parse()
+ .map_err(RouteParseError::ParseGatewayAddress)
+ .map_err(Error::ParseRouteError)?;
+ let tun_gateway_ip6: Option<IpAddr> = if let Some(gateway) = env.get("ifconfig_ipv6_remote") {
+ Some(
+ gateway
+ .parse()
+ .map_err(RouteParseError::ParseGatewayAddress)
+ .map_err(Error::ParseRouteError)?,
+ )
+ } else {
+ None
+ };
+
+ let tun_interface = env.get("dev").ok_or(Error::MissingTunnelInterface)?;
+ let tun_node = routing::NetNode::from(routing::Node::new(
+ tun_gateway_ip,
+ tun_interface.to_string(),
+ ));
+ #[cfg(windows)]
+ let tun_node6 = if tun_gateway_ip6.is_some() {
+ // The tapdrvr expects a special address here rather than a real gateway.
+ // See https://github.com/OpenVPN/openvpn/blob/23e11e591347080efa3b933beca7f620dd059d5c/src/openvpn/route.c#L2013
+ routing::NetNode::from(routing::Node::new(
+ "fe80::8"
+ .parse()
+ .map_err(RouteParseError::ParseGatewayAddress)
+ .map_err(Error::ParseRouteError)?,
+ tun_interface.to_string(),
+ ))
+ } else {
+ routing::NetNode::from(routing::Node::device(tun_interface.to_string()))
+ };
+ #[cfg(not(windows))]
+ let tun_node6 = routing::NetNode::from(routing::Node::device(tun_interface.to_string()));
+
+ let ovpn_routes = parse_openvpn_dict_routes(env).map_err(Error::ParseRouteError)?;
+
+ for route in ovpn_routes {
+ let node = if route.gateway == default_node_ip {
+ routing::NetNode::DefaultNode
+ } else if route.gateway == tun_gateway_ip {
+ tun_node.clone()
+ } else if Some(route.gateway) == tun_gateway_ip6 {
+ tun_node6.clone()
+ } else {
+ routing::NetNode::from(routing::Node::address(route.gateway))
+ };
+ routes.insert(RequiredRoute::new(route.network, node));
+ }
+
+ if env.get("socks_proxy_server_1").is_none() {
+ let remote_host: IpAddr = env
+ .get("remote_1")
+ .ok_or(Error::MissingRemoteHost)?
+ .parse()
+ .map_err(Error::ParseRemoteHost)?;
+ routes.insert(RequiredRoute::new(
+ remote_host.into(),
+ routing::NetNode::DefaultNode,
+ ));
+ }
+
+ #[cfg(target_os = "linux")]
+ let tun_node = routing::Node::device(tun_interface.to_string());
+ for network in &["0.0.0.0/1".parse().unwrap(), "128.0.0.0/1".parse().unwrap()] {
+ routes.insert(RequiredRoute::new(*network, tun_node.clone()));
+ }
+
+ Ok(routes)
+}
+
impl<C: OpenVpnBuilder + 'static> OpenVpnMonitor<C> {
fn new_internal<L>(
mut cmd: C,