diff options
| author | Emīls Piņķis <emils@mullvad.net> | 2018-11-22 15:14:49 +0000 |
|---|---|---|
| committer | Emīls Piņķis <emils@mullvad.net> | 2018-11-22 15:14:49 +0000 |
| commit | 507df9e5fb93c8e28bea5d1ef0475a6bd55749c6 (patch) | |
| tree | 85ea6193b6f452e093fe48edaa20f0ceac5b6b60 | |
| parent | 1a5099d9b437e0d94373e7389905280a440b35a0 (diff) | |
| parent | 9c9fab7212275833b0fcbe49b8d086acdb072a54 (diff) | |
| download | mullvadvpn-507df9e5fb93c8e28bea5d1ef0475a6bd55749c6.tar.xz mullvadvpn-507df9e5fb93c8e28bea5d1ef0475a6bd55749c6.zip | |
Merge branch 'add-routing'
| -rw-r--r-- | talpid-core/src/lib.rs | 4 | ||||
| -rw-r--r-- | talpid-core/src/routing/linux.rs | 300 | ||||
| -rw-r--r-- | talpid-core/src/routing/macos.rs | 125 | ||||
| -rw-r--r-- | talpid-core/src/routing/mod.rs | 103 | ||||
| -rw-r--r-- | talpid-core/src/routing/subprocess.rs | 46 |
5 files changed, 578 insertions, 0 deletions
diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs index 20c9302c8a..c570d94f85 100644 --- a/talpid-core/src/lib.rs +++ b/talpid-core/src/lib.rs @@ -43,6 +43,10 @@ extern crate talpid_types; #[cfg(windows)] mod winnet; +#[cfg(unix)] +/// Abstraction over operating system routing table. +pub mod routing; + mod offline; /// Working with processes. diff --git a/talpid-core/src/routing/linux.rs b/talpid-core/src/routing/linux.rs new file mode 100644 index 0000000000..9f33c9401d --- /dev/null +++ b/talpid-core/src/routing/linux.rs @@ -0,0 +1,300 @@ +use super::{NetNode, RequiredRoutes}; + +use super::subprocess::{Exec, RunExpr}; +use std::collections::HashSet; +use std::net::IpAddr; + + +error_chain! { + errors { + FailedToAddRoute { + description("Failed to add route") + } + FailedToRemoveRoute { + description("Failed to remove route") + } + + FailedToRemoveTable { + description("Failed to remove table") + } + + FailedToAdjustMainRoutingTable { + description("Failed to adjust main routing table") + } + + FailedToSetRuleForFwmark { + description("Failed to set rule for fwmark") + } + NoDefaultRoute { + description("No default route") + } + + FailedToGetDefaultRoute { + description("Failed to get default route") + } + } +} + +#[derive(Hash, Eq, PartialEq)] +enum IpVersion { + V4, + V6, +} + +impl IpVersion { + fn new(ip: IpAddr) -> Self { + if ip.is_ipv4() { + IpVersion::V4 + } else { + IpVersion::V6 + } + } + + fn is_ipv4(&self) -> bool { + match self { + IpVersion::V4 => true, + _ => false, + } + } +} + +impl From<IpAddr> for IpVersion { + fn from(ip: IpAddr) -> IpVersion { + Self::new(ip) + } +} + +impl AsRef<str> for IpVersion { + fn as_ref(&self) -> &str { + match self { + IpVersion::V4 => "-4", + IpVersion::V6 => "-6", + } + } +} + +// A record of a table being set by a RouteManager. +#[derive(Hash, Eq, PartialEq)] +struct Table { + version: IpVersion, + fwmark: String, +} + +pub struct RouteManager { + added_routes: HashSet<super::Route>, + added_tables: HashSet<Table>, + // the main routing table only has to be adjusted for default routes + main_table_suppress_by_prefix_set_v4: bool, + main_table_suppress_by_prefix_set_v6: bool, +} + +impl RouteManager { + // This function adjusts main routing table to not make any routing decisions based on rules + // with a prefix of 0. This is to bypass the main table for default routes. + fn set_suppress_prefix_length_on_main_routing_table( + &mut self, + version: IpVersion, + set_rule: bool, + ) -> Result<()> { + if (version.is_ipv4() && (set_rule == self.main_table_suppress_by_prefix_set_v4)) + || (!version.is_ipv4() && (set_rule == self.main_table_suppress_by_prefix_set_v6)) + { + return Ok(()); + } + duct::cmd!( + "ip", + version.as_ref(), + "rule", + if set_rule { "add" } else { "delete" }, + "table", + "main", + "suppress_prefixlength", + "0" + ) + .run_expr() + .chain_err(|| ErrorKind::FailedToAdjustMainRoutingTable)?; + if version.is_ipv4() { + self.main_table_suppress_by_prefix_set_v4 = set_rule; + } else { + self.main_table_suppress_by_prefix_set_v6 = set_rule; + } + Ok(()) + } + + fn add_route(&mut self, route: super::Route, fwmark: &Option<String>) -> Result<()> { + if route.prefix.prefix() == 0 { + self.set_suppress_prefix_length_on_main_routing_table(route.prefix.ip().into(), true)?; + } + + let version = IpVersion::new(route.prefix.ip()); + + let mut cmd = Exec::cmd("ip") + .arg(version.as_ref()) + .arg("route") + .arg("add") + .arg(route.prefix.to_string()); + cmd = match &route.node { + NetNode::Address(ref addr) => cmd.arg(addr.to_string()), + NetNode::Device(ref device) => cmd.arg("dev").arg(device), + }; + + if let Some(ref fwmark) = &fwmark { + cmd = cmd.arg("table").arg(fwmark); + } + + cmd.to_expr() + .run_expr() + .chain_err(|| ErrorKind::FailedToAddRoute)?; + + if let Some(fwmark) = &fwmark { + self.ensure_table_rules(Table { + version, + fwmark: fwmark.to_string(), + })?; + } else { + self.added_routes.insert(route); + } + Ok(()) + } + + // if a route we're applying is set to a specific table, that table should have it's rules set + fn ensure_table_rules(&mut self, added_table: Table) -> Result<()> { + if self.added_tables.contains(&added_table) { + return Ok(()); + } + duct::cmd!( + "ip", + added_table.version.as_ref(), + "rule", + "add", + "not", + "fwmark", + &added_table.fwmark, + "table", + &added_table.fwmark + ) + .run_expr() + .chain_err(|| ErrorKind::FailedToSetRuleForFwmark)?; + + + self.added_tables.insert(added_table); + Ok(()) + } + + fn clear_routes(&mut self) -> Result<()> { + let mut end_result = Ok(()); + for route in self.added_routes.drain() { + let ip_vers: IpVersion = route.prefix.ip().into(); + let result = duct::cmd!( + "ip", + ip_vers.as_ref(), + "route", + "delete", + route.prefix.to_string() + ) + .run_expr() + .chain_err(|| ErrorKind::FailedToRemoveRoute); + if let Err(e) = result { + log::error!("Failed to remove route {} - {}", route.prefix, e); + end_result = Err(e); + } + } + end_result + } + + fn clear_tables(&mut self) -> Result<()> { + let mut end_result = Ok(()); + for table in self.added_tables.drain() { + let result = duct::cmd!( + "ip", + table.version.as_ref(), + "rule", + "delete", + "table", + &table.fwmark + ) + .run_expr() + .chain_err(|| ErrorKind::FailedToRemoveTable); + + if let Err(e) = result { + log::error!("Failed to remove routing table {} - {}", &table.fwmark, e); + end_result = Err(e); + } + } + + if self.main_table_suppress_by_prefix_set_v4 { + if let Err(e) = + self.set_suppress_prefix_length_on_main_routing_table(IpVersion::V4, false) + { + log::error!( + "Failed to remove prefix limit for main routing table - {}", + e + ); + end_result = Err(e); + } else { + self.main_table_suppress_by_prefix_set_v4 = false; + } + } + + if self.main_table_suppress_by_prefix_set_v6 { + if let Err(e) = + self.set_suppress_prefix_length_on_main_routing_table(IpVersion::V6, false) + { + log::error!( + "Failed to remove prefix limit for main routing table - {}", + e + ); + end_result = Err(e); + } else { + self.main_table_suppress_by_prefix_set_v6 = false; + } + } + end_result + } +} + +impl super::RoutingT for RouteManager { + type Error = Error; + fn new() -> Result<Self> { + Ok(RouteManager { + added_routes: HashSet::new(), + added_tables: HashSet::new(), + // the main routing table only has to be adjusted for default routes + main_table_suppress_by_prefix_set_v4: false, + main_table_suppress_by_prefix_set_v6: false, + }) + } + + fn add_routes(&mut self, required_routes: RequiredRoutes) -> Result<()> { + for route in required_routes.routes.into_iter() { + if let Err(e) = self.add_route(route, &required_routes.fwmark) { + let _ = self.delete_routes(); + return Err(e); + } + } + Ok(()) + } + + fn delete_routes(&mut self) -> Result<()> { + let result = self.clear_routes(); + let other_result = self.clear_tables(); + result.and_then(|_| other_result) + } + + /// Retrieves the gateway for the default route + fn get_default_route_node(&mut self) -> Result<IpAddr> { + let output = duct::cmd!("ip", "route") + .stdout() + .chain_err(|| ErrorKind::FailedToGetDefaultRoute)?; + let ip_str: &str = output + .lines() + .find(|line| line.trim().starts_with("default via ")) + .and_then(|line| line.trim().split_whitespace().skip(2).next()) + .map(Ok) + .unwrap_or(Err(Error::from(ErrorKind::FailedToGetDefaultRoute)))?; + + ip_str + .parse() + .map_err(|_| Error::from(ErrorKind::FailedToGetDefaultRoute)) + } +} diff --git a/talpid-core/src/routing/macos.rs b/talpid-core/src/routing/macos.rs new file mode 100644 index 0000000000..43081b21bb --- /dev/null +++ b/talpid-core/src/routing/macos.rs @@ -0,0 +1,125 @@ +use super::{NetNode, RequiredRoutes, Route}; + +use super::subprocess::{Exec, RunExpr}; +use std::collections::HashSet; +use std::net::IpAddr; + +error_chain! { + errors { + FailedToAddRoute { + description("Failed to add route") + } + + FailedToGetDefaultRoute { + description("Failed to get default route") + } + + FailedToRemoveRoute { + description("Failed to remove route") + } + } +} + +pub struct RouteManager { + set_routes: HashSet<Route>, +} + +impl RouteManager { + fn add_route(&mut self, route: Route) -> Result<()> { + if route.prefix.prefix() == 0 { + if route.prefix.is_ipv4() { + self.add_route(Route::new("0.0.0.0/1".parse().unwrap(), route.node.clone()))?; + self.add_route(Route::new( + "128.0.0.0/1".parse().unwrap(), + route.node.clone(), + ))?; + } else { + self.add_route(Route::new("::/1".parse().unwrap(), route.node.clone()))?; + self.add_route(Route::new("8000::/1".parse().unwrap(), route.node.clone()))?; + } + }; + + let mut cmd = Exec::cmd("route") + .arg("-q") + .arg("-n") + .arg("add") + .arg(ip_vers(&route)) + .arg(route.prefix.to_string()); + cmd = match &route.node { + NetNode::Address(ref addr) => cmd.arg("-gateway").arg(addr.to_string()), + NetNode::Device(device) => cmd.arg("-interface").arg(&device), + }; + + cmd.to_expr() + .run_expr() + .chain_err(|| ErrorKind::FailedToAddRoute)?; + self.set_routes.insert(route); + Ok(()) + } +} + +fn ip_vers(route: &Route) -> &'static str { + if route.prefix.is_ipv4() { + "-inet" + } else { + "-inet6" + } +} + +impl super::RoutingT for RouteManager { + type Error = Error; + + fn new() -> Result<Self> { + Ok(Self { + set_routes: HashSet::new(), + }) + } + + fn add_routes(&mut self, required_routes: RequiredRoutes) -> Result<()> { + for route in required_routes.routes.into_iter() { + if let Err(e) = self.add_route(route) { + let _ = self.delete_routes(); + return Err(e); + } + } + Ok(()) + } + + fn delete_routes(&mut self) -> Result<()> { + let mut end_result = Ok(()); + for route in self.set_routes.drain() { + let result = duct::cmd!( + "route", + "-q", + "-n", + "delete", + ip_vers(&route), + route.prefix.to_string() + ) + .run_expr() + .chain_err(|| ErrorKind::FailedToRemoveRoute); + if let Err(e) = result { + log::error!("failed to reset remove route: {}", e); + end_result = Err(e); + } + } + // returning the last error as to signal some kind of failure. + end_result + } + + + fn get_default_route_node(&mut self) -> Result<IpAddr> { + let output = duct::cmd!("route", "-n", "get", "default") + .stdout() + .chain_err(|| ErrorKind::FailedToGetDefaultRoute)?; + let ip_str: &str = output + .lines() + .find(|line| line.trim().starts_with("gateway: ")) + .and_then(|line| line.trim().split_whitespace().skip(1).next()) + .ok_or(Error::from(ErrorKind::FailedToGetDefaultRoute))?; + + ip_str + .parse() + .map_err(|_| Error::from(ErrorKind::FailedToGetDefaultRoute)) + } +} diff --git a/talpid-core/src/routing/mod.rs b/talpid-core/src/routing/mod.rs new file mode 100644 index 0000000000..5a8fb34e64 --- /dev/null +++ b/talpid-core/src/routing/mod.rs @@ -0,0 +1,103 @@ +use ipnetwork::IpNetwork; +use std::net::IpAddr; + +#[cfg(target_os = "macos")] +#[path = "macos.rs"] +mod imp; + +#[cfg(target_os = "linux")] +#[path = "linux.rs"] +mod imp; + +mod subprocess; + + +/// A single route +#[derive(Hash, Eq, PartialEq)] +pub struct Route { + /// Route prefix + pub prefix: IpNetwork, + /// Route node + pub node: NetNode, +} + +impl Route { + /// Create a new route + pub fn new(prefix: IpNetwork, node: NetNode) -> Self { + Self { prefix, node } + } +} + +/// A network node for a given route +#[derive(Hash, Eq, PartialEq, Clone)] +pub enum NetNode { + /// For routing something through a network host + Address(IpAddr), + /// For routing something through an interface + Device(String), +} + +/// Contains a set of routes to be added +pub struct RequiredRoutes { + /// List of routes to be applied to the routing table. + pub routes: Vec<Route>, + /// Optionally apply the routes to a specific table and only apply routes when a firewall mark + /// is not used. Currently only used on Linux. + pub fwmark: Option<String>, +} + +/// Manages adding and removing routes from the routing table. +pub struct RouteManager { + inner: imp::RouteManager, +} + +impl RouteManager { + /// Creates a new RouteManager. + pub fn new() -> Result<Self, imp::Error> { + Ok(RouteManager { + inner: imp::RouteManager::new()?, + }) + } + + /// Set routes in the routing table. + pub fn add_routes(&mut self, required_routes: RequiredRoutes) -> Result<(), imp::Error> { + self.inner.add_routes(required_routes) + } + + /// Remove previously set routes from the routing table. + pub fn delete_routes(&mut self) -> Result<(), imp::Error> { + self.inner.delete_routes() + } + + /// Retrieves the gateway for the default route. + pub fn get_default_route_node(&mut self) -> Result<std::net::IpAddr, imp::Error> { + self.inner.get_default_route_node() + } +} + +impl Drop for RouteManager { + fn drop(&mut self) { + if let Err(e) = self.delete_routes() { + log::error!("Failed to reset routes on drop - {}", e); + } + } +} + +/// This trait unifies platform specific implementations of route managers +pub trait RoutingT: Sized { + /// Error type of the implementation + type Error: ::std::error::Error; + + /// Creates a new router + fn new() -> Result<Self, Self::Error>; + + /// Adds routes to the system routing table. + fn add_routes(&mut self, required_routes: RequiredRoutes) -> Result<(), Self::Error>; + + /// Removes previously set routes. If routes were set for specific tables, the whole tables + /// will be removed. + fn delete_routes(&mut self) -> Result<(), Self::Error>; + + /// Retrieves the gateway for the default route + fn get_default_route_node(&mut self) -> Result<std::net::IpAddr, Self::Error>; +} diff --git a/talpid-core/src/routing/subprocess.rs b/talpid-core/src/routing/subprocess.rs new file mode 100644 index 0000000000..d569f0191b --- /dev/null +++ b/talpid-core/src/routing/subprocess.rs @@ -0,0 +1,46 @@ +use duct::Expression; +use std::ffi::{OsStr, OsString}; + +pub trait RunExpr: Sized { + fn run_expr(self) -> ::std::io::Result<()>; + fn stdout(self) -> ::std::io::Result<String>; +} + + +impl RunExpr for Expression { + fn run_expr(self) -> ::std::io::Result<()> { + log::trace!("Executing command - {:?}", self); + self.run().map(|_| ()) + } + + fn stdout(self) -> ::std::io::Result<String> { + log::trace!("Executing command - {:?}", self); + self.stdout_capture() + .run() + .map(|output| String::from_utf8_lossy(&output.stdout).into_owned()) + } +} + + +pub struct Exec { + cmd: OsString, + args: Vec<OsString>, +} + +impl Exec { + pub fn cmd<S: AsRef<OsStr>>(cmd: S) -> Exec { + Exec { + cmd: cmd.as_ref().to_owned(), + args: vec![], + } + } + + pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Exec { + self.args.push(arg.as_ref().to_owned()); + self + } + + pub fn to_expr(self) -> Expression { + duct::cmd(self.cmd, self.args) + } +} |
