summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls Piņķis <emils@mullvad.net>2018-11-22 15:14:49 +0000
committerEmīls Piņķis <emils@mullvad.net>2018-11-22 15:14:49 +0000
commit507df9e5fb93c8e28bea5d1ef0475a6bd55749c6 (patch)
tree85ea6193b6f452e093fe48edaa20f0ceac5b6b60
parent1a5099d9b437e0d94373e7389905280a440b35a0 (diff)
parent9c9fab7212275833b0fcbe49b8d086acdb072a54 (diff)
downloadmullvadvpn-507df9e5fb93c8e28bea5d1ef0475a6bd55749c6.tar.xz
mullvadvpn-507df9e5fb93c8e28bea5d1ef0475a6bd55749c6.zip
Merge branch 'add-routing'
-rw-r--r--talpid-core/src/lib.rs4
-rw-r--r--talpid-core/src/routing/linux.rs300
-rw-r--r--talpid-core/src/routing/macos.rs125
-rw-r--r--talpid-core/src/routing/mod.rs103
-rw-r--r--talpid-core/src/routing/subprocess.rs46
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)
+ }
+}