diff options
| author | David Lönnhager <david.l@mullvad.net> | 2020-05-14 16:10:35 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2020-06-02 10:05:02 +0200 |
| commit | 7eaf1fe790937d1dbdff39206b04fd51a6c6ff06 (patch) | |
| tree | bc82fec26726969d0169669e4db91a78c7bff3fe | |
| parent | 7731393aae9f476b4e64593b4357a17bab9c4f68 (diff) | |
| download | mullvadvpn-7eaf1fe790937d1dbdff39206b04fd51a6c6ff06.tar.xz mullvadvpn-7eaf1fe790937d1dbdff39206b04fd51a6c6ff06.zip | |
Set up split tunneling routes in routing module
| -rw-r--r-- | talpid-core/src/routing/linux.rs | 215 | ||||
| -rw-r--r-- | talpid-core/src/routing/unix.rs | 85 | ||||
| -rw-r--r-- | talpid-core/src/split.rs | 199 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connected_state.rs | 8 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connecting_state.rs | 5 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/disconnected_state.rs | 7 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/mod.rs | 9 |
7 files changed, 302 insertions, 226 deletions
diff --git a/talpid-core/src/routing/linux.rs b/talpid-core/src/routing/linux.rs index 7a7bfa4865..04dc47fd46 100644 --- a/talpid-core/src/routing/linux.rs +++ b/talpid-core/src/routing/linux.rs @@ -1,12 +1,18 @@ -use crate::routing::{imp::RouteManagerCommand, NetNode, Node, RequiredRoute, Route}; +use crate::{ + routing::{imp::RouteManagerCommand, NetNode, Node, RequiredRoute, Route}, + split, +}; use talpid_types::ErrorExt; use ipnetwork::IpNetwork; +use regex::Regex; use std::{ collections::{BTreeMap, HashSet}, - io, - net::IpAddr, + fs, + io::{self, BufRead, BufReader, Read, Seek, Write}, + net::{IpAddr, Ipv4Addr}, + process::Command, thread, }; @@ -36,6 +42,9 @@ use rtnetlink::{ use libc::{AF_INET, AF_INET6}; +const ROUTING_TABLE_NAME: &str = "mullvad_exclusions"; +const RT_TABLES_PATH: &str = "/etc/iproute2/rt_tables"; + pub type Result<T> = std::result::Result<T, Error>; @@ -67,8 +76,24 @@ pub enum Error { #[error(display = "Unknown device index - {}", _0)] UnknownDeviceIndex(u32), + /// Unable to create routing table for tagged connections and packets. + #[error(display = "Unable to create routing table for split tunneling")] + ExclusionsRoutingTableSetup(#[error(source)] io::Error), + + /// Unable to create routing table for tagged connections and packets. + #[error(display = "Cannot find a free routing table ID in rt_tables")] + NoFreeRoutingTableId, + #[error(display = "Shutting down route manager")] Shutdown, + + /// Failed to run the process. + #[error(display = "Unable to execute process")] + ExecFailed(#[error(source)] io::Error), + + /// ip command returned an error status. + #[error(display = "ip command failed")] + IpFailed, } pub struct RouteManagerImpl { @@ -146,6 +171,8 @@ pub struct RouteManagerImplInner { default_routes: HashSet<Route>, best_default_node_v4: Option<Node>, best_default_node_v6: Option<Node>, + + split_table_id: i32, } impl RouteManagerImplInner { @@ -163,6 +190,7 @@ impl RouteManagerImplInner { tokio02::spawn(connection); let iface_map = Self::initialize_link_map(&handle).await?; + let split_table_id = Self::initialize_exclusions_table().await?; let mut monitor = Self { iface_map, @@ -175,6 +203,8 @@ impl RouteManagerImplInner { default_routes: HashSet::new(), best_default_node_v4: None, best_default_node_v6: None, + + split_table_id, }; monitor.default_routes = monitor.get_default_routes().await?; @@ -188,6 +218,155 @@ impl RouteManagerImplInner { Ok(monitor) } + /// Set up policy-based routing table for marked packets. + /// Returns the routing table id. + async fn initialize_exclusions_table() -> Result<i32> { + // Add routing table to /etc/iproute2/rt_tables, if it does not exist + + let file = fs::OpenOptions::new() + .read(true) + .open(RT_TABLES_PATH) + .map_err(Error::ExclusionsRoutingTableSetup)?; + let buf_reader = BufReader::new(file); + let expression = Regex::new(r"^\s*(\d+)\s+(\w+)").unwrap(); + + let mut used_ids = Vec::<i32>::new(); + + for line in buf_reader.lines() { + let line = line.map_err(Error::ExclusionsRoutingTableSetup)?; + if let Some(captures) = expression.captures(&line) { + let table_id = captures + .get(1) + .unwrap() + .as_str() + .parse::<i32>() + .expect("Table ID does not fit i32"); + let table_name = captures.get(2).unwrap().as_str(); + + if table_name == ROUTING_TABLE_NAME { + // The table has already been added + return Ok(table_id); + } + + used_ids.push(table_id); + } + } + + used_ids.sort_unstable(); + + // Assign a free id to the table + let mut table_id = 1; + loop { + if used_ids.binary_search(&table_id).is_err() { + break; + } + + table_id += 1; + + if table_id >= 256 { + return Err(Error::NoFreeRoutingTableId); + } + } + + let mut file = fs::OpenOptions::new() + .read(true) + .append(true) + .open(RT_TABLES_PATH) + .map_err(Error::ExclusionsRoutingTableSetup)?; + + if let Ok(_) = file.seek(io::SeekFrom::End(-1)) { + // Append newline if necessary + let mut buffer = [0u8]; + let _ = file.read_exact(&mut buffer); + if buffer[0] != b'\n' { + writeln!(file).map_err(Error::ExclusionsRoutingTableSetup)?; + } + } + + writeln!(file, "{} {}", table_id, ROUTING_TABLE_NAME) + .map_err(Error::ExclusionsRoutingTableSetup)?; + Ok(table_id) + } + + /// Route PID-associated packets through the physical interface. + async fn enable_exclusions_routes(&mut self) -> Result<()> { + // TODO: IPv6 + + let table_id_str = &self.split_table_id.to_string(); + + // Create the rule if it does not exist + let mut cmd = Command::new("ip"); + cmd.args(&["-4", "rule", "list", "table", table_id_str]); + log::trace!("running cmd - {:?}", &cmd); + let out = cmd.output().map_err(Error::ExecFailed)?; + + let missing_rule = + !out.status.success() || String::from_utf8_lossy(&out.stdout).trim().is_empty(); + if missing_rule { + exec_ip(&[ + "-4", + "rule", + "add", + "from", + "all", + "fwmark", + &split::MARK.to_string(), + "lookup", + table_id_str, + ])?; + } + + // Add default route for the exclusions table + let zero_network = + ipnetwork::IpNetwork::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0).unwrap(); + let mut required_routes = HashSet::new(); + required_routes.insert( + RequiredRoute::new(zero_network, NetNode::DefaultNode).table(self.split_table_id as u8), + ); + self.add_required_routes(required_routes).await + } + + /// Stop routing PID-associated packets through the physical interface. + async fn disable_exclusions_routes(&self) { + // TODO: IPv6 + + if let Err(e) = exec_ip(&[ + "-4", + "rule", + "del", + "from", + "all", + "fwmark", + &split::MARK.to_string(), + "lookup", + &self.split_table_id.to_string(), + ]) { + log::warn!("Failed to delete routing policy: {}", e); + } + } + + /// Route DNS requests through the tunnel interface. + #[cfg(target_os = "linux")] + async fn route_exclusions_dns( + &mut self, + tunnel_alias: &str, + dns_servers: &[IpAddr], + ) -> Result<()> { + let mut dns_routes = HashSet::new(); + + for server in dns_servers { + dns_routes.insert( + RequiredRoute::new( + IpNetwork::from(*server), + Node::device(tunnel_alias.to_string()), + ) + .table(self.split_table_id as u8), + ); + } + + self.add_required_routes(dns_routes).await + } + async fn add_required_default_routes( &mut self, required_default_routes: HashSet<RequiredDefaultRoute>, @@ -477,11 +656,17 @@ impl RouteManagerImplInner { } RouteManagerCommand::AddRoutes(routes, result_rx) => { log::debug!("Adding routes: {:?}", routes); - if let Err(error) = self.add_required_routes(routes.clone()).await { - let _ = result_rx.send(Err(error)); - } else { - let _ = result_rx.send(Ok(())); - } + let _ = result_rx.send(self.add_required_routes(routes.clone()).await); + } + RouteManagerCommand::EnableExclusionsRoutes(result_rx) => { + let _ = result_rx.send(self.enable_exclusions_routes().await); + } + RouteManagerCommand::DisableExclusionsRoutes => { + self.disable_exclusions_routes().await; + } + RouteManagerCommand::RouteExclusionsDns(tunnel_alias, dns_servers, result_rx) => { + let _ = + result_rx.send(self.route_exclusions_dns(&tunnel_alias, &dns_servers).await); } RouteManagerCommand::ClearRoutes => { log::debug!("Clearing routes"); @@ -760,6 +945,20 @@ fn ip_to_bytes(addr: IpAddr) -> Vec<u8> { } } +fn exec_ip(args: &[&str]) -> Result<()> { + let mut cmd = Command::new("ip"); + cmd.args(args); + + log::trace!("running cmd - {:?}", &cmd); + + let status = cmd.status().map_err(Error::ExecFailed)?; + if status.success() { + Ok(()) + } else { + Err(Error::IpFailed) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/talpid-core/src/routing/unix.rs b/talpid-core/src/routing/unix.rs index 3f50ef3db5..e7193facb1 100644 --- a/talpid-core/src/routing/unix.rs +++ b/talpid-core/src/routing/unix.rs @@ -12,6 +12,9 @@ use futures01::{ use std::{collections::HashSet, sync::mpsc::sync_channel}; use talpid_types::ErrorExt; +#[cfg(target_os = "linux")] +use std::net::IpAddr; + #[cfg(target_os = "macos")] #[path = "macos.rs"] mod imp; @@ -51,6 +54,16 @@ pub enum RouteManagerCommand { ), ClearRoutes, Shutdown(oneshot::Sender<()>), + #[cfg(target_os = "linux")] + EnableExclusionsRoutes(oneshot::Sender<Result<(), PlatformError>>), + #[cfg(target_os = "linux")] + DisableExclusionsRoutes, + #[cfg(target_os = "linux")] + RouteExclusionsDns( + String, + Vec<IpAddr>, + oneshot::Sender<Result<(), PlatformError>>, + ), } /// RouteManager applies a set of routes to the route table. @@ -147,6 +160,78 @@ impl RouteManager { Err(Error::RouteManagerDown) } } + + /// Route PID-associated packets through the physical interface. + #[cfg(target_os = "linux")] + pub fn enable_exclusions_routes(&self) -> Result<(), Error> { + if let Some(tx) = &self.manage_tx { + let (result_tx, result_rx) = oneshot::channel(); + if tx + .unbounded_send(RouteManagerCommand::EnableExclusionsRoutes(result_tx)) + .is_err() + { + return Err(Error::RouteManagerDown); + } + + match result_rx.wait() { + Ok(result) => result.map_err(Error::PlatformError), + Err(error) => { + log::trace!("{}", error.display_chain_with_msg("channel is closed")); + Ok(()) + } + } + } else { + Err(Error::RouteManagerDown) + } + } + + /// Stop routing PID-associated packets through the physical interface. + #[cfg(target_os = "linux")] + pub fn disable_exclusions_routes(&self) -> Result<(), Error> { + if let Some(tx) = &self.manage_tx { + if tx + .unbounded_send(RouteManagerCommand::DisableExclusionsRoutes) + .is_err() + { + return Err(Error::RouteManagerDown); + } + Ok(()) + } else { + Err(Error::RouteManagerDown) + } + } + + /// Route DNS requests through the tunnel interface. + #[cfg(target_os = "linux")] + pub fn route_exclusions_dns( + &mut self, + tunnel_alias: &str, + dns_servers: &[IpAddr], + ) -> Result<(), Error> { + if let Some(tx) = &self.manage_tx { + let (result_tx, result_rx) = oneshot::channel(); + if tx + .unbounded_send(RouteManagerCommand::RouteExclusionsDns( + tunnel_alias.to_string(), + dns_servers.to_vec(), + result_tx, + )) + .is_err() + { + return Err(Error::RouteManagerDown); + } + + match result_rx.wait() { + Ok(result) => result.map_err(Error::PlatformError), + Err(error) => { + log::trace!("{}", error.display_chain_with_msg("channel is closed")); + Ok(()) + } + } + } else { + Err(Error::RouteManagerDown) + } + } } impl Drop for RouteManager { diff --git a/talpid-core/src/split.rs b/talpid-core/src/split.rs index bf0ec8e7b0..d9054ad4d8 100644 --- a/talpid-core/src/split.rs +++ b/talpid-core/src/split.rs @@ -1,14 +1,8 @@ #![cfg(target_os = "linux")] -use crate::routing::{NetNode, Node, RequiredRoute, RouteManager}; -use ipnetwork::IpNetwork; -use regex::Regex; use std::{ - collections::HashSet, fs, - io::{self, BufRead, BufReader, BufWriter, Read, Seek, Write}, - net::{IpAddr, Ipv4Addr}, + io::{self, BufRead, BufReader, BufWriter, Write}, path::Path, - process::Command, }; use talpid_types::SPLIT_TUNNEL_CGROUP_NAME; @@ -21,25 +15,10 @@ pub const NETCLS_CLASSID: u32 = 0x4d9f41; /// This should be an arbitrary but unique integer. pub const MARK: i32 = 0xf41; -const ROUTING_TABLE_NAME: &str = "mullvad_exclusions"; -const RT_TABLES_PATH: &str = "/etc/iproute2/rt_tables"; - /// Errors related to split tunneling. #[derive(err_derive::Error, Debug)] #[error(no_from)] pub enum Error { - /// Failed to run the process. - #[error(display = "Unable to execute process")] - ExecFailed(#[error(source)] io::Error), - - /// ip command returned an error status. - #[error(display = "ip command failed")] - IpFailed, - - /// Unable to create routing table for tagged connections and packets. - #[error(display = "Unable to create routing table")] - RoutingTableSetup(#[error(source)] io::Error), - /// Unable to create cgroup. #[error(display = "Unable to initialize net_cls cgroup instance")] InitNetClsCGroup(#[error(source)] nix::Error), @@ -63,168 +42,6 @@ pub enum Error { /// Unable to read cgroup.procs. #[error(display = "Unable to obtain PIDs from cgroup.procs")] ListCGroupPids(#[error(source)] io::Error), - - /// Unable to add route to the exclusions table. - #[error(display = "Failed to add routing table entry")] - SetupRouting(#[error(source)] crate::routing::Error), - - /// Unable to add setup DNS routing. - #[error(display = "Failed to add routing table DNS rules")] - SetDns(#[error(source)] crate::routing::Error), -} - -/// Manage routing for split tunneling cgroup. -pub struct SplitTunnel { - table_id: i32, -} - -impl SplitTunnel { - /// Object that allows specified applications to not pass through the tunnel - pub fn new() -> Result<SplitTunnel, Error> { - let mut tunnel = SplitTunnel { table_id: 0 }; - tunnel.initialize_routing_table()?; - Ok(tunnel) - } - - /// Set up policy-based routing for marked packets. - fn initialize_routing_table(&mut self) -> Result<(), Error> { - // Add routing table to /etc/iproute2/rt_tables, if it does not exist - - let file = fs::OpenOptions::new() - .read(true) - .open(RT_TABLES_PATH) - .map_err(Error::RoutingTableSetup)?; - let buf_reader = BufReader::new(file); - let expression = Regex::new(r"^\s*(\d+)\s+(\w+)").unwrap(); - - let mut used_ids = Vec::<i32>::new(); - - for line in buf_reader.lines() { - let line = line.map_err(Error::RoutingTableSetup)?; - if let Some(captures) = expression.captures(&line) { - let table_id = captures - .get(1) - .unwrap() - .as_str() - .parse::<i32>() - .expect("Table ID does not fit i32"); - let table_name = captures.get(2).unwrap().as_str(); - - if table_name == ROUTING_TABLE_NAME { - // The table has already been added - self.table_id = table_id; - return Ok(()); - } - - used_ids.push(table_id); - } - } - - used_ids.sort_unstable(); - for id in 1..256 { - if used_ids.binary_search(&id).is_err() { - // Assign a free id to the table - self.table_id = id; - break; - } - } - - let mut file = fs::OpenOptions::new() - .read(true) - .append(true) - .open(RT_TABLES_PATH) - .map_err(Error::RoutingTableSetup)?; - - if let Ok(_) = file.seek(io::SeekFrom::End(-1)) { - // Append newline if necessary - let mut buffer = [0u8]; - let _ = file.read_exact(&mut buffer); - if buffer[0] != b'\n' { - writeln!(file).map_err(Error::RoutingTableSetup)?; - } - } - - writeln!(file, "{} {}", self.table_id, ROUTING_TABLE_NAME).map_err(Error::RoutingTableSetup) - } - - /// Route PID-associated packets through the physical interface. - pub fn enable_routing(&self, route_manager: &mut RouteManager) -> Result<(), Error> { - // TODO: IPv6 - - // Create the rule if it does not exist - let mut cmd = Command::new("ip"); - cmd.args(&["-4", "rule", "list", "table", ROUTING_TABLE_NAME]); - log::trace!("running cmd - {:?}", &cmd); - let out = cmd.output().map_err(Error::ExecFailed)?; - - let missing_rule = - !out.status.success() || String::from_utf8_lossy(&out.stdout).trim().is_empty(); - if missing_rule { - exec_ip(&[ - "-4", - "rule", - "add", - "from", - "all", - "fwmark", - &MARK.to_string(), - "lookup", - ROUTING_TABLE_NAME, - ])?; - } - - // Add default route for the exclusions table - let zero_network = - ipnetwork::IpNetwork::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0).unwrap(); - let mut required_routes = HashSet::new(); - required_routes.insert( - RequiredRoute::new(zero_network, NetNode::DefaultNode).table(self.table_id as u8), - ); - route_manager - .add_routes(required_routes) - .map_err(Error::SetupRouting) - } - - /// Stop routing PID-associated packets through the physical interface. - pub fn disable_routing(&self) { - // TODO: IPv6 - - if let Err(e) = exec_ip(&[ - "-4", - "rule", - "del", - "from", - "all", - "fwmark", - &MARK.to_string(), - "lookup", - ROUTING_TABLE_NAME, - ]) { - log::warn!("Failed to delete routing policy: {}", e); - } - } - - /// Route DNS requests through the tunnel interface. - pub fn route_dns( - &self, - route_manager: &mut RouteManager, - tunnel_alias: &str, - dns_servers: &[IpAddr], - ) -> Result<(), Error> { - let mut dns_routes = HashSet::new(); - - for server in dns_servers { - dns_routes.insert( - RequiredRoute::new( - IpNetwork::from(*server), - Node::device(tunnel_alias.to_string()), - ) - .table(self.table_id as u8), - ); - } - - route_manager.add_routes(dns_routes).map_err(Error::SetDns) - } } /// Manages PIDs to exclude from the tunnel. @@ -343,17 +160,3 @@ impl PidManager { Ok(()) } } - -fn exec_ip(args: &[&str]) -> Result<(), Error> { - let mut cmd = Command::new("ip"); - cmd.args(args); - - log::trace!("running cmd - {:?}", &cmd); - - let status = cmd.status().map_err(Error::ExecFailed)?; - if status.success() { - Ok(()) - } else { - Err(Error::IpFailed) - } -} diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index ef5362d635..6ba26007b7 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -82,12 +82,8 @@ impl ConnectedState { #[cfg(target_os = "linux")] shared_values - .split_tunnel - .route_dns( - &mut shared_values.route_manager, - &self.metadata.interface, - &dns_ips, - ) + .route_manager + .route_exclusions_dns(&self.metadata.interface, &dns_ips) .map_err(BoxedError::new)?; Ok(()) diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 3a34f7fb0f..13a7104aa1 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -359,10 +359,7 @@ impl TunnelState for ConnectingState { ErrorState::enter(shared_values, ErrorStateCause::StartTunnelError) } else { #[cfg(target_os = "linux")] - if let Err(error) = shared_values - .split_tunnel - .enable_routing(&mut shared_values.route_manager) - { + if let Err(error) = shared_values.route_manager.enable_exclusions_routes() { error!( "{}", error.display_chain_with_msg("Failed to set up split tunneling") diff --git a/talpid-core/src/tunnel_state_machine/disconnected_state.rs b/talpid-core/src/tunnel_state_machine/disconnected_state.rs index ac3b33aed5..0b1005a355 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -40,7 +40,12 @@ impl TunnelState for DisconnectedState { _: Self::Bootstrap, ) -> (TunnelStateWrapper, TunnelStateTransition) { #[cfg(target_os = "linux")] - shared_values.split_tunnel.disable_routing(); + if let Err(error) = shared_values.route_manager.disable_exclusions_routes() { + log::error!( + "{}", + error.display_chain_with_msg("Failed to disable exclusions routes") + ); + } Self::set_firewall_policy(shared_values); #[cfg(target_os = "android")] shared_values.tun_provider.close_tun(); diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index 0393be3418..d1225496e1 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -14,8 +14,6 @@ use self::{ disconnecting_state::{AfterDisconnect, DisconnectingState}, error_state::ErrorState, }; -#[cfg(target_os = "linux")] -use crate::split; use crate::{ dns::DnsMonitor, firewall::{Firewall, FirewallArguments}, @@ -243,17 +241,12 @@ impl TunnelStateMachine { } }; - #[cfg(target_os = "linux")] - let split_tunnel = split::SplitTunnel::new().map_err(Error::InitSplitTunneling)?; - let firewall = Firewall::new(args).map_err(Error::InitFirewallError)?; let dns_monitor = DnsMonitor::new(cache_dir).map_err(Error::InitDnsMonitorError)?; let route_manager = RouteManager::new(HashSet::new()).map_err(Error::InitRouteManagerError)?; let mut shared_values = SharedTunnelStateValues { firewall, - #[cfg(target_os = "linux")] - split_tunnel, dns_monitor, route_manager, allow_lan, @@ -338,8 +331,6 @@ pub trait TunnelParametersGenerator: Send + 'static { /// Values that are common to all tunnel states. struct SharedTunnelStateValues { firewall: Firewall, - #[cfg(target_os = "linux")] - split_tunnel: split::SplitTunnel, dns_monitor: DnsMonitor, route_manager: RouteManager, /// Should LAN access be allowed outside the tunnel. |
