summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls Piņķis <emils@mullvad.net>2018-11-21 11:14:35 +0000
committerEmīls Piņķis <emils@mullvad.net>2018-11-22 14:45:46 +0000
commit0ba8f28909dd0ee4d5ccd076c8e2c053f33850d4 (patch)
tree86ffc780dade950fa2954f30bcd035c6ab8a7218
parent24dd2218bb443d6a2395a1a59c892a1000d5580a (diff)
downloadmullvadvpn-0ba8f28909dd0ee4d5ccd076c8e2c053f33850d4.tar.xz
mullvadvpn-0ba8f28909dd0ee4d5ccd076c8e2c053f33850d4.zip
Add linux routing implementation
-rw-r--r--talpid-core/src/routing/linux.rs300
-rw-r--r--talpid-core/src/routing/mod.rs41
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