diff options
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 13 | ||||
| -rw-r--r-- | talpid-core/src/split.rs | 424 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connected_state.rs | 13 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connecting_state.rs | 2 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/disconnected_state.rs | 4 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/mod.rs | 8 |
6 files changed, 245 insertions, 219 deletions
diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index bcb045f7c3..2b92162b2c 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -96,6 +96,9 @@ pub enum Error { #[error(display = "Unable to load account history with wireguard key cache")] LoadAccountHistory(#[error(source)] account_history::Error), + #[error(display = "Unable to initialize split tunneling")] + InitSplitTunneling(#[error(source)] split::Error), + #[error(display = "No wireguard private key available")] NoKeyAvailable, @@ -444,6 +447,7 @@ pub struct Daemon<L: EventListener> { tunnel_state: TunnelState, target_state: TargetState, state: DaemonExecutionState, + exclude_pids: split::PidManager, rx: Wait<UnboundedReceiver<InternalDaemonEvent>>, tx: DaemonEventSender, reconnection_loop_tx: Option<mpsc::Sender<()>>, @@ -587,6 +591,7 @@ where tunnel_state: TunnelState::Disconnected, target_state: initial_target_state, state: DaemonExecutionState::Running, + exclude_pids: split::PidManager::new().map_err(Error::InitSplitTunneling)?, rx: internal_event_rx.wait(), tx: internal_event_tx, reconnection_loop_tx: None, @@ -1378,7 +1383,7 @@ where #[cfg(unix)] fn on_get_split_tunnel_processes(&mut self, tx: oneshot::Sender<Vec<i32>>) { - match split::list_pids() { + match self.exclude_pids.list() { Ok(pids) => Self::oneshot_send(tx, pids, "get_split_tunnel_processes response"), Err(e) => error!("{}", e.display_chain_with_msg("Unable to obtain PIDs")), } @@ -1386,7 +1391,7 @@ where #[cfg(unix)] fn on_add_split_tunnel_process(&mut self, tx: oneshot::Sender<()>, pid: i32) { - match split::add_pid(pid) { + match self.exclude_pids.add(pid) { Ok(()) => Self::oneshot_send(tx, (), "add_split_tunnel_process response"), Err(e) => error!("{}", e.display_chain_with_msg("Unable to add PID")), } @@ -1394,7 +1399,7 @@ where #[cfg(unix)] fn on_remove_split_tunnel_process(&mut self, tx: oneshot::Sender<()>, pid: i32) { - match split::remove_pid(pid) { + match self.exclude_pids.remove(pid) { Ok(()) => Self::oneshot_send(tx, (), "remove_split_tunnel_process response"), Err(e) => error!("{}", e.display_chain_with_msg("Unable to remove PID")), } @@ -1402,7 +1407,7 @@ where #[cfg(unix)] fn on_clear_split_tunnel_processes(&mut self, tx: oneshot::Sender<()>) { - match split::clear_pids() { + match self.exclude_pids.clear() { Ok(()) => Self::oneshot_send(tx, (), "clear_split_tunnel_processes response"), Err(e) => error!("{}", e.display_chain_with_msg("Unable to clear PIDs")), } diff --git a/talpid-core/src/split.rs b/talpid-core/src/split.rs index 678197a416..8973a6724c 100644 --- a/talpid-core/src/split.rs +++ b/talpid-core/src/split.rs @@ -1,7 +1,7 @@ use regex::Regex; use std::{ fs, - io::{self, BufRead, BufReader, Write}, + io::{self, BufRead, BufReader, BufWriter, Write}, net::{AddrParseError, IpAddr}, path::Path, process::Command, @@ -70,6 +70,7 @@ struct DefaultRoute { address: IpAddr, } +/// Obtain the IP/interface of the physical interface fn get_default_route() -> Result<DefaultRoute, Error> { // FIXME: use netlink let mut cmd = Command::new("ip"); @@ -96,261 +97,282 @@ fn get_default_route() -> Result<DefaultRoute, Error> { Err(Error::NoDefaultRoute) } -fn reset_table() -> Result<(), Error> { - // Flush table - let mut cmd = Command::new("ip"); - cmd.args(&["-4", "route", "flush", "table", ROUTING_TABLE_NAME]); +/// Manage routing for split tunneling cgroup. +pub struct SplitTunnel; - log::trace!("running cmd - {:?}", &cmd); - cmd.output().map_err(Error::RoutingTableSetup)?; +impl SplitTunnel { + /// Object that allows specified applications to not pass through the tunnel + pub fn new() -> Result<SplitTunnel, Error> { + Self::initialize_routing_table()?; + Ok(SplitTunnel {}) + } - // Force routing through the physical interface - let default_route = get_default_route()?; - let mut cmd = Command::new("ip"); - cmd.args(&[ - "-4", - "route", - "add", - "default", - "via", - &default_route.address.to_string(), - "dev", - &default_route.interface, - "table", - ROUTING_TABLE_NAME, - ]); + /// Set up policy-based routing for marked packets. + fn initialize_routing_table() -> Result<(), Error> { + // TODO: ensure the ID does not conflict with that of another table - log::trace!("running cmd - {:?}", &cmd); - cmd.output().map(|_| ()).map_err(Error::RoutingTableSetup) -} + // Add routing table to /etc/iproute2/rt_tables, if it does not exist -/// Route PID-associated packets through the physical interface. -pub fn route_marked_packets() -> Result<(), Error> { - // TODO: IPv6 + let mut file = fs::OpenOptions::new() + .read(true) + .append(true) + .create(true) + .open(RT_TABLES_PATH) + .map_err(Error::RoutingTableSetup)?; + let buf_reader = BufReader::new(file.try_clone().map_err(Error::RoutingTableSetup)?); + let expression = Regex::new(r"^\s*(\d+)\s+(\w+)").unwrap(); - // 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::RoutingTableSetup)?; - let out = if !out.status.success() { - "" - } else { - std::str::from_utf8(&out.stdout) - .map_err(|_| { - Error::RoutingTableSetup(io::Error::new( - io::ErrorKind::InvalidData, - "Error parsing ip output", - )) - })? - .trim() - }; + 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(); + + // Already added + if table_name == ROUTING_TABLE_NAME { + if table_id != unsafe { ROUTING_TABLE_ID } { + unsafe { ROUTING_TABLE_ID = table_id }; + } + + return Ok(()); + } + } + } + + write!( + file, + "{} {}", + unsafe { ROUTING_TABLE_ID }, + ROUTING_TABLE_NAME + ) + .map_err(Error::RoutingTableSetup) + } + + /// Reset the split-tunneling routing table to its default state + fn reset_table() -> Result<(), Error> { + let mut cmd = Command::new("ip"); + cmd.args(&["-4", "route", "flush", "table", ROUTING_TABLE_NAME]); + + log::trace!("running cmd - {:?}", &cmd); + cmd.output().map_err(Error::RoutingTableSetup)?; - if out == "" { + // Force routing through the physical interface + let default_route = get_default_route()?; let mut cmd = Command::new("ip"); cmd.args(&[ "-4", - "rule", + "route", "add", - "from", - "all", - "fwmark", - &MARK.to_string(), - "lookup", + "default", + "via", + &default_route.address.to_string(), + "dev", + &default_route.interface, + "table", ROUTING_TABLE_NAME, ]); log::trace!("running cmd - {:?}", &cmd); - cmd.output().map_err(Error::RoutingTableSetup)?; + cmd.output().map(|_| ()).map_err(Error::RoutingTableSetup) } - reset_table() -} + /// Route PID-associated packets through the physical interface. + pub fn enable_routing(&self) -> Result<(), Error> { + // TODO: IPv6 -/// Stop routing PID-associated packets through the physical interface. -pub fn disable_routing() -> Result<(), Error> { - // TODO: IPv6 - - let mut cmd = Command::new("ip"); - cmd.args(&[ - "-4", - "rule", - "del", - "from", - "all", - "fwmark", - &MARK.to_string(), - "lookup", - ROUTING_TABLE_NAME, - ]); - - log::trace!("running cmd - {:?}", &cmd); - let out = cmd.output(); - if out.is_err() { - log::warn!("Failed to delete routing policy: {}", out.err().unwrap()); - } else { - let out = out.unwrap(); - if !out.status.success() { - log::warn!( - "Failed to delete routing policy: {}", - String::from_utf8_lossy(&out.stderr) - ); - } - } - - Ok(()) -} - -/// Route DNS requests through the tunnel interface. -pub fn route_dns(tunnel_alias: &str, dns_servers: &[IpAddr]) -> Result<(), Error> { - reset_table()?; - - for server in dns_servers { - if let IpAddr::V4(addr) = server { - let addr = addr.to_string(); + // 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::RoutingTableSetup)?; + let out = if !out.status.success() { + "" + } else { + std::str::from_utf8(&out.stdout) + .map_err(|_| { + Error::RoutingTableSetup(io::Error::new( + io::ErrorKind::InvalidData, + "Error parsing ip output", + )) + })? + .trim() + }; + if out == "" { let mut cmd = Command::new("ip"); cmd.args(&[ "-4", - "route", + "rule", "add", - &addr, - "via", - &addr, - "dev", - tunnel_alias, - "table", + "from", + "all", + "fwmark", + &MARK.to_string(), + "lookup", ROUTING_TABLE_NAME, ]); log::trace!("running cmd - {:?}", &cmd); - cmd.output().map_err(Error::SetDns)?; + cmd.output().map_err(Error::RoutingTableSetup)?; } + + Self::reset_table() } - Ok(()) -} + /// Stop routing PID-associated packets through the physical interface. + pub fn disable_routing(&self) -> Result<(), Error> { + // TODO: IPv6 -/// Reset DNS rules. -pub fn flush_dns() -> Result<(), Error> { - // For now, simply flush it - reset_table() -} + let mut cmd = Command::new("ip"); + cmd.args(&[ + "-4", + "rule", + "del", + "from", + "all", + "fwmark", + &MARK.to_string(), + "lookup", + ROUTING_TABLE_NAME, + ]); -/// Set up policy-based routing for marked packets. -pub fn initialize_routing_table() -> Result<(), Error> { - // TODO: ensure the ID does not conflict with that of another table + log::trace!("running cmd - {:?}", &cmd); + let out = cmd.output(); + if out.is_err() { + log::warn!("Failed to delete routing policy: {}", out.err().unwrap()); + } else { + let out = out.unwrap(); + if !out.status.success() { + log::warn!( + "Failed to delete routing policy: {}", + String::from_utf8_lossy(&out.stderr) + ); + } + } - // Add routing table to /etc/iproute2/rt_tables, if it does not exist + Ok(()) + } - let mut file = fs::OpenOptions::new() - .read(true) - .append(true) - .create(true) - .open(RT_TABLES_PATH) - .map_err(Error::RoutingTableSetup)?; - let buf_reader = BufReader::new(file.try_clone().map_err(Error::RoutingTableSetup)?); - let expression = Regex::new(r"^\s*(\d+)\s+(\w+)").unwrap(); + /// Route DNS requests through the tunnel interface. + pub fn route_dns(&self, tunnel_alias: &str, dns_servers: &[IpAddr]) -> Result<(), Error> { + for server in dns_servers { + if let IpAddr::V4(addr) = server { + let addr = addr.to_string(); - 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(); + let mut cmd = Command::new("ip"); + cmd.args(&[ + "-4", + "route", + "replace", + &addr, + "dev", + tunnel_alias, + "table", + ROUTING_TABLE_NAME, + ]); - // Already added - if table_name == ROUTING_TABLE_NAME { - if table_id != unsafe { ROUTING_TABLE_ID } { - unsafe { ROUTING_TABLE_ID = table_id }; - } - - return Ok(()); + log::trace!("running cmd - {:?}", &cmd); + cmd.output().map_err(Error::SetDns)?; } } + + Ok(()) } - write!( - file, - "{} {}", - unsafe { ROUTING_TABLE_ID }, - ROUTING_TABLE_NAME - ) - .map_err(Error::RoutingTableSetup) + /// Reset DNS rules. + pub fn flush_dns(&self) -> Result<(), Error> { + // For now, simply flush it + Self::reset_table() + } } -/// Set up cgroup used to track PIDs for split tunneling. -pub fn create_cgroup() -> Result<(), Error> { - let exclusions_dir = Path::new(NETCLS_DIR).join(CGROUP_NAME); +/// Manages PIDs to exclude from the tunnel. +pub struct PidManager; - if !exclusions_dir.exists() { - fs::create_dir(exclusions_dir.clone()).map_err(Error::CreateCGroup)?; +impl PidManager { + /// Create object to manage split-tunnel PIDs. + pub fn new() -> Result<PidManager, Error> { + Self::create_cgroup()?; + Ok(PidManager {}) } - let classid_path = exclusions_dir.join("net_cls.classid"); - fs::write(classid_path, NETCLS_CLASSID.to_string().as_bytes()).map_err(Error::SetCGroupClassId) -} + /// Set up cgroup used to track PIDs for split tunneling. + fn create_cgroup() -> Result<(), Error> { + let exclusions_dir = Path::new(NETCLS_DIR).join(CGROUP_NAME); -/// Add a PID to exclude from the tunnel. -pub fn add_pid(pid: i32) -> Result<(), Error> { - let exclusions_path = Path::new(NETCLS_DIR).join(CGROUP_NAME).join("cgroup.procs"); + if !exclusions_dir.exists() { + fs::create_dir(exclusions_dir.clone()).map_err(Error::CreateCGroup)?; + } - let mut file = fs::OpenOptions::new() - .write(true) - .create(true) - .open(exclusions_path) - .map_err(Error::AddCGroupPid)?; + let classid_path = exclusions_dir.join("net_cls.classid"); + fs::write(classid_path, NETCLS_CLASSID.to_string().as_bytes()) + .map_err(Error::SetCGroupClassId) + } - file.write_all(pid.to_string().as_bytes()) - .map_err(Error::AddCGroupPid) -} + /// Add a PID to exclude from the tunnel. + pub fn add(&self, pid: i32) -> Result<(), Error> { + let exclusions_path = Path::new(NETCLS_DIR).join(CGROUP_NAME).join("cgroup.procs"); -/// Remove a PID from processes to exclude from the tunnel. -pub fn remove_pid(pid: i32) -> Result<(), Error> { - // FIXME: We remove PIDs from our cgroup here by adding - // them to the parent cgroup. This seems wrong. - let exclusions_path = Path::new(NETCLS_DIR).join("cgroup.procs"); + let mut file = fs::OpenOptions::new() + .write(true) + .create(true) + .open(exclusions_path) + .map_err(Error::AddCGroupPid)?; - let mut file = fs::OpenOptions::new() - .write(true) - .create(true) - .open(exclusions_path) - .map_err(Error::RemoveCGroupPid)?; + file.write_all(pid.to_string().as_bytes()) + .map_err(Error::AddCGroupPid) + } - file.write_all(pid.to_string().as_bytes()) - .map_err(Error::RemoveCGroupPid) -} + /// Remove a PID from processes to exclude from the tunnel. + pub fn remove(&self, pid: i32) -> Result<(), Error> { + // FIXME: We remove PIDs from our cgroup here by adding + // them to the parent cgroup. This seems wrong. + let exclusions_path = Path::new(NETCLS_DIR).join(CGROUP_NAME).join("cgroup.procs"); -/// Return a list of PIDs that are excluded from the tunnel. -pub fn list_pids() -> Result<Vec<i32>, Error> { - let exclusions_path = Path::new(NETCLS_DIR).join(CGROUP_NAME).join("cgroup.procs"); + let mut file = fs::OpenOptions::new() + .write(true) + .create(true) + .open(exclusions_path) + .map_err(Error::RemoveCGroupPid)?; - let file = fs::File::open(exclusions_path).map_err(Error::ListCGroupPids)?; + file.write_all(pid.to_string().as_bytes()) + .map_err(Error::RemoveCGroupPid) + } - let result: Result<Vec<i32>, io::Error> = BufReader::new(file) - .lines() - .map(|line| { - line.and_then(|v| { - v.parse() - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) - }) - }) - .collect(); - result.map_err(Error::ListCGroupPids) -} + /// Return a list of PIDs that are excluded from the tunnel. + pub fn list(&self) -> Result<Vec<i32>, Error> { + // TODO: manage child PIDs somehow? -/// Clear list of PIDs to exclude from the tunnel. -pub fn clear_pids() -> Result<(), Error> { - // TODO: reuse file handle - let pids = list_pids()?; + let exclusions_path = Path::new(NETCLS_DIR).join(CGROUP_NAME).join("cgroup.procs"); - for pid in pids { - remove_pid(pid)?; + let file = fs::File::open(exclusions_path).map_err(Error::ListCGroupPids)?; + + let result: Result<Vec<i32>, io::Error> = BufReader::new(file) + .lines() + .map(|line| { + line.and_then(|v| { + v.parse() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) + }) + }) + .collect(); + result.map_err(Error::ListCGroupPids) } - Ok(()) + /// Clear list of PIDs to exclude from the tunnel. + pub fn clear(&self) -> Result<(), Error> { + // TODO: reuse file handle + let pids = self.list()?; + + for pid in pids { + self.remove(pid)?; + } + + Ok(()) + } } diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index e936499f47..55c4a28697 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -4,7 +4,6 @@ use super::{ }; use crate::{ firewall::FirewallPolicy, - split, tunnel::{CloseHandle, TunnelEvent, TunnelMetadata}, }; use futures01::{ @@ -81,11 +80,14 @@ impl ConnectedState { .set(&self.metadata.interface, &dns_ips) .map_err(BoxedError::new)?; - split::route_dns(&self.metadata.interface, &dns_ips).map_err(BoxedError::new) + shared_values + .split_tunnel + .route_dns(&self.metadata.interface, &dns_ips) + .map_err(BoxedError::new) } fn reset_dns(shared_values: &mut SharedTunnelStateValues) { - if let Err(error) = split::flush_dns() { + if let Err(error) = shared_values.split_tunnel.flush_dns() { log::error!( "{}", error.display_chain_with_msg("Unable to update split-tunnel route") @@ -243,10 +245,7 @@ impl TunnelState for ConnectedState { ), ) } else if let Err(error) = connected_state.set_dns(shared_values) { - log::error!( - "{}", - error.display_chain_with_msg("Failed to set system DNS settings") - ); + log::error!("{}", error.display_chain_with_msg("Failed to set DNS")); DisconnectingState::enter( shared_values, ( diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index e43f9203fe..da61626de1 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -358,7 +358,7 @@ impl TunnelState for ConnectingState { ) ); ErrorState::enter(shared_values, ErrorStateCause::StartTunnelError) - } else if let Err(error) = split::route_marked_packets() { + } else if let Err(error) = shared_values.split_tunnel.enable_routing() { 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 8277f89013..7742f276ad 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -2,7 +2,7 @@ use super::{ ConnectingState, ErrorState, EventConsequence, SharedTunnelStateValues, TunnelCommand, TunnelState, TunnelStateTransition, TunnelStateWrapper, }; -use crate::{firewall::FirewallPolicy, split}; +use crate::firewall::FirewallPolicy; use futures01::{sync::mpsc, Stream}; use talpid_types::ErrorExt; @@ -39,7 +39,7 @@ impl TunnelState for DisconnectedState { shared_values: &mut SharedTunnelStateValues, _: Self::Bootstrap, ) -> (TunnelStateWrapper, TunnelStateTransition) { - if let Err(error) = split::disable_routing() { + if let Err(error) = shared_values.split_tunnel.disable_routing() { log::error!( "{}", error.display_chain_with_msg("Failed to update routing") diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index f1aae86017..3515a72529 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -242,10 +242,7 @@ impl TunnelStateMachine { }; #[cfg(unix)] - { - split::initialize_routing_table().map_err(Error::InitSplitTunneling)?; - split::create_cgroup().map_err(Error::InitSplitTunneling)?; - } + 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)?; @@ -253,6 +250,8 @@ impl TunnelStateMachine { RouteManager::new(HashSet::new()).map_err(Error::InitRouteManagerError)?; let mut shared_values = SharedTunnelStateValues { firewall, + #[cfg(unix)] + split_tunnel, dns_monitor, route_manager, allow_lan, @@ -337,6 +336,7 @@ pub trait TunnelParametersGenerator: Send + 'static { /// Values that are common to all tunnel states. struct SharedTunnelStateValues { firewall: Firewall, + split_tunnel: split::SplitTunnel, dns_monitor: DnsMonitor, route_manager: RouteManager, /// Should LAN access be allowed outside the tunnel. |
