diff options
| author | Emīls Piņķis <emils@mullvad.net> | 2018-11-21 11:14:35 +0000 |
|---|---|---|
| committer | Emīls Piņķis <emils@mullvad.net> | 2018-11-22 14:45:46 +0000 |
| commit | 0ba8f28909dd0ee4d5ccd076c8e2c053f33850d4 (patch) | |
| tree | 86ffc780dade950fa2954f30bcd035c6ab8a7218 | |
| parent | 24dd2218bb443d6a2395a1a59c892a1000d5580a (diff) | |
| download | mullvadvpn-0ba8f28909dd0ee4d5ccd076c8e2c053f33850d4.tar.xz mullvadvpn-0ba8f28909dd0ee4d5ccd076c8e2c053f33850d4.zip | |
Add linux routing implementation
| -rw-r--r-- | talpid-core/src/routing/linux.rs | 300 | ||||
| -rw-r--r-- | talpid-core/src/routing/mod.rs | 41 |
2 files changed, 341 insertions, 0 deletions
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/mod.rs b/talpid-core/src/routing/mod.rs index 29fa8f6f8b..b2a4d379ad 100644 --- a/talpid-core/src/routing/mod.rs +++ b/talpid-core/src/routing/mod.rs @@ -1,6 +1,10 @@ use ipnetwork::IpNetwork; use std::net::IpAddr; +#[cfg(target_os = "linux")] +#[path = "linux.rs"] +mod imp; + mod subprocess; @@ -38,6 +42,43 @@ pub struct RequiredRoutes { 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 |
