summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLinus Färnstrand <linus@mullvad.net>2018-06-26 15:33:46 +0200
committerLinus Färnstrand <linus@mullvad.net>2018-07-02 12:16:33 +0200
commitf04ffa0a8393d42f63813a71485ee043966417f1 (patch)
tree1c39fa6f3908458ba5a1613d51f54cceb2be4596
parent0b7db4052b312e8f6d9d6458c8f34388f7746a4e (diff)
downloadmullvadvpn-f04ffa0a8393d42f63813a71485ee043966417f1.tar.xz
mullvadvpn-f04ffa0a8393d42f63813a71485ee043966417f1.zip
Add Linux netfilter firewall integration
-rw-r--r--README.md6
-rw-r--r--talpid-core/Cargo.toml7
-rw-r--r--talpid-core/src/firewall/linux/mod.rs436
-rw-r--r--talpid-core/src/firewall/macos/mod.rs2
-rw-r--r--talpid-core/src/firewall/mod.rs4
-rw-r--r--talpid-core/src/lib.rs5
6 files changed, 447 insertions, 13 deletions
diff --git a/README.md b/README.md
index 1bd128bdcc..1828525206 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,12 @@ homebrew:
It must run as root since it it modifies the firewall and sets up virtual network interfaces
etc.
+### Environment variables controlling the execution
+
+* `TALPID_NFTABLES_COUNTERS` - Set to `"1"` to add packet counters to all firewall rules on
+ Linux.
+
+
## Building and running the Electron GUI app
1. Install all the JavaScript dependencies by running:
diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml
index cf0c8b6ea8..4f2f68fed8 100644
--- a/talpid-core/Cargo.toml
+++ b/talpid-core/Cargo.toml
@@ -15,17 +15,19 @@ jsonrpc-macros = { git = "https://github.com/paritytech/jsonrpc", tag = "v8.0.1"
lazy_static = "1.0"
libc = "0.2.20"
log = "0.4"
+openvpn-plugin = { version = "0.3", features = ["serde"] }
os_pipe = "0.6"
-uuid = { version = "0.6", features = ["v4"] }
shell-escape = "0.1"
+uuid = { version = "0.6", features = ["v4"] }
-openvpn-plugin = { version = "0.3", features = ["serde"] }
talpid-ipc = { path = "../talpid-ipc" }
talpid-types = { path = "../talpid-types" }
[target.'cfg(target_os = "linux")'.dependencies]
notify = "4.0"
resolv-conf = "0.6.1"
+nftnl = { git = "https://github.com/mullvad/nftnl-rs", features = ["nftnl-1-1-0"] }
+mnl = { git = "https://github.com/mullvad/mnl-rs", features = ["mnl-1-0-4"] }
[target.'cfg(target_os = "macos")'.dependencies]
pfctl = "0.2"
@@ -34,7 +36,6 @@ core-foundation = "0.5"
tokio-core = "0.1"
[target.'cfg(windows)'.dependencies]
-libc = "0.2.20"
widestring = "0.3"
[dev-dependencies]
diff --git a/talpid-core/src/firewall/linux/mod.rs b/talpid-core/src/firewall/linux/mod.rs
index 7761afd7e9..8c4c39be08 100644
--- a/talpid-core/src/firewall/linux/mod.rs
+++ b/talpid-core/src/firewall/linux/mod.rs
@@ -1,21 +1,76 @@
+extern crate mnl;
+
use error_chain::ChainedError;
+
+use ipnetwork::IpNetwork;
+use libc;
+use nftnl::{
+ self,
+ expr::{self, InterfaceName, Verdict},
+ Batch, Chain, FinalizedBatch, ProtoFamily, Rule, Table,
+};
+use talpid_types::net;
+use tunnel;
+
+use std::env;
+use std::ffi::CString;
+use std::io;
+use std::net::{IpAddr, Ipv4Addr};
use std::path::Path;
use super::{Firewall, SecurityPolicy};
mod dns;
-
use self::dns::DnsSettings;
error_chain! {
+ errors {
+ /// Error when opening a netlink socket to netfilter
+ NetlinkOpenError { description("Unable to open netlink socket") }
+ /// Error when writing to netlink socket
+ NetlinkSendError { description("Unable to send netlink command to netfilter") }
+ /// Error when reading from netlink socket
+ NetlinkRecvError { description("Error while reading from netlink socket") }
+ }
links {
DnsSettings(self::dns::Error, self::dns::ErrorKind) #[doc = "DNS error"];
+ Nftnl(nftnl::Error, nftnl::ErrorKind) #[doc = "Error in nftnl"];
+ }
+ foreign_links {
+ Netlink(io::Error) #[doc = "Error in mnl"];
}
}
+lazy_static! {
+ /// TODO(linus): This crate is not supposed to be Mullvad-aware. So at some point this should be
+ /// replaced by allowing the table name to be configured from the public API of this crate.
+ static ref TABLE_NAME: CString = CString::new("mullvad").unwrap();
+ static ref IN_CHAIN_NAME: CString = CString::new("in").unwrap();
+ static ref OUT_CHAIN_NAME: CString = CString::new("out").unwrap();
+
+ /// Allows controlling whether firewall rules should have packet counters or not from an env
+ /// variable. Useful for debugging the rules.
+ static ref ADD_COUNTERS: bool = env::var("TALPID_NFTABLES_COUNTERS")
+ .map(|v| v == "1")
+ .unwrap_or(false);
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+enum Direction {
+ In,
+ Out,
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+enum End {
+ Src,
+ Dst,
+}
+
/// The Linux implementation for the `Firewall` trait.
pub struct Netfilter {
dns_settings: DnsSettings,
+ table: Table,
}
impl Firewall for Netfilter {
@@ -24,25 +79,392 @@ impl Firewall for Netfilter {
fn new<P: AsRef<Path>>(_cache_dir: P) -> Result<Self> {
Ok(Netfilter {
dns_settings: DnsSettings::new()?,
+ table: Table::new(&*TABLE_NAME, ProtoFamily::Inet)?,
})
}
fn apply_policy(&mut self, policy: SecurityPolicy) -> Result<()> {
+ if let SecurityPolicy::Connected { ref tunnel, .. } = policy {
+ self.dns_settings.set_dns(vec![tunnel.gateway.into()])?;
+ }
+
+ let batch = PolicyBatch::new(&self.table)?.finalize(&policy)?;
+ self.send_and_process(&batch)
+ }
+
+ fn reset_policy(&mut self) -> Result<()> {
+ if let Err(error) = self.dns_settings.reset() {
+ error!("Failed to reset DNS settings: {}", error.display_chain());
+ }
+
+ let batch = {
+ let mut batch = Batch::new()?;
+ // Our batch will add and remove the table even though the goal is just to remove it.
+ // This because only removing it throws a strange error if the table does not exist.
+ batch.add(&self.table, nftnl::MsgType::Add)?;
+ batch.add(&self.table, nftnl::MsgType::Del)?;
+ batch.finalize()?
+ };
+
+ debug!("Removing table and chain from netfilter");
+ self.send_and_process(&batch)
+ }
+}
+
+impl Netfilter {
+ fn send_and_process(&self, batch: &FinalizedBatch) -> Result<()> {
+ let socket =
+ mnl::Socket::new(mnl::Bus::Netfilter).chain_err(|| ErrorKind::NetlinkOpenError)?;
+ socket
+ .send_all(batch)
+ .chain_err(|| ErrorKind::NetlinkSendError)?;
+
+ let portid = socket.portid();
+ let mut buffer = vec![0; nftnl::nft_nlmsg_maxsize() as usize];
+
+
+ while let Some(message) = Self::socket_recv(&socket, &mut buffer[..])? {
+ match mnl::cb_run(message, 2, portid)? {
+ mnl::CbResult::Stop => {
+ trace!("cb_run STOP");
+ break;
+ }
+ mnl::CbResult::Ok => trace!("cb_run OK"),
+ }
+ }
+
+ Ok(())
+ }
+
+ fn socket_recv<'a>(socket: &mnl::Socket, buf: &'a mut [u8]) -> Result<Option<&'a [u8]>> {
+ let ret = socket.recv(buf).chain_err(|| ErrorKind::NetlinkRecvError)?;
+ trace!("Read {} bytes from netlink", ret);
+ if ret > 0 {
+ Ok(Some(&buf[..ret]))
+ } else {
+ Ok(None)
+ }
+ }
+}
+
+struct PolicyBatch<'a> {
+ batch: Batch,
+ in_chain: Chain<'a>,
+ out_chain: Chain<'a>,
+}
+
+impl<'a> PolicyBatch<'a> {
+ /// Bootstrap a new nftnl message batch object and add the initial messages creating the
+ /// table and chains.
+ pub fn new(table: &'a Table) -> Result<Self> {
+ let mut batch = Batch::new()?;
+ let mut out_chain = Chain::new(&*OUT_CHAIN_NAME, table)?;
+ let mut in_chain = Chain::new(&*IN_CHAIN_NAME, table)?;
+ out_chain.set_hook(nftnl::Hook::Out, 0);
+ in_chain.set_hook(nftnl::Hook::In, 0);
+ out_chain.set_policy(nftnl::Policy::Drop);
+ in_chain.set_policy(nftnl::Policy::Drop);
+
+ batch.add(table, nftnl::MsgType::Add)?;
+ batch.add(table, nftnl::MsgType::Del)?;
+ batch.add(table, nftnl::MsgType::Add)?;
+ batch.add(&out_chain, nftnl::MsgType::Add)?;
+ batch.add(&in_chain, nftnl::MsgType::Add)?;
+
+ Ok(PolicyBatch {
+ batch,
+ in_chain,
+ out_chain,
+ })
+ }
+
+ /// Finalize the nftnl message batch by adding every firewall rule needed to satisfy the given
+ /// policy.
+ pub fn finalize(mut self, policy: &SecurityPolicy) -> Result<FinalizedBatch> {
+ self.add_loopback_rules()?;
+ self.add_dhcp_rules()?;
+ self.add_policy_specific_rules(policy)?;
+
+ Ok(self.batch.finalize()?)
+ }
+
+ fn add_loopback_rules(&mut self) -> Result<()> {
+ let loopback_device = InterfaceName::Exact(CString::new("lo").unwrap());
+ self.batch.add(
+ &allow_interface_rule(&self.out_chain, Direction::Out, &loopback_device)?,
+ nftnl::MsgType::Add,
+ )?;
+ self.batch.add(
+ &allow_interface_rule(&self.in_chain, Direction::In, &loopback_device)?,
+ nftnl::MsgType::Add,
+ )?;
+ Ok(())
+ }
+
+ fn add_dhcp_rules(&mut self) -> Result<()> {
+ self.batch.add(
+ &allow_dhcp_rule(&self.out_chain, Direction::Out)?,
+ nftnl::MsgType::Add,
+ )?;
+ self.batch.add(
+ &allow_dhcp_rule(&self.in_chain, Direction::In)?,
+ nftnl::MsgType::Add,
+ )?;
+ Ok(())
+ }
+
+ fn add_policy_specific_rules(&mut self, policy: &SecurityPolicy) -> Result<()> {
match policy {
- SecurityPolicy::Connected { tunnel, .. } => {
- self.dns_settings.set_dns(vec![tunnel.gateway.into()])?;
+ SecurityPolicy::Connecting {
+ relay_endpoint,
+ allow_lan,
+ } => {
+ self.add_allow_endpoint_rules(relay_endpoint)?;
+ if *allow_lan {
+ self.add_allow_lan_rules()?;
+ }
+ }
+ SecurityPolicy::Connected {
+ relay_endpoint,
+ tunnel,
+ allow_lan,
+ } => {
+ self.add_allow_endpoint_rules(relay_endpoint)?;
+ self.add_dns_rule(tunnel, net::TransportProtocol::Udp)?;
+ self.add_dns_rule(tunnel, net::TransportProtocol::Tcp)?;
+ self.add_allow_tunnel_rules(tunnel)?;
+ if *allow_lan {
+ self.add_allow_lan_rules()?;
+ }
}
- _ => (),
}
+ Ok(())
+ }
+
+ fn add_allow_endpoint_rules(&mut self, endpoint: &net::Endpoint) -> Result<()> {
+ let mut in_rule = Rule::new(&self.in_chain)?;
+ check_endpoint(&mut in_rule, End::Src, endpoint)?;
+
+ in_rule.add_expr(nft_expr!(ct state))?;
+ let allowed_states = nftnl::expr::ct::States::ESTABLISHED.bits();
+ in_rule.add_expr(nft_expr!(bitwise mask allowed_states, xor 0u32))?;
+ in_rule.add_expr(nft_expr!(cmp != 0u32))?;
+ add_verdict(&mut in_rule, Verdict::Accept)?;
+
+ self.batch.add(&in_rule, nftnl::MsgType::Add)?;
+
+
+ let mut out_rule = Rule::new(&self.out_chain)?;
+ check_endpoint(&mut out_rule, End::Dst, endpoint)?;
+ add_verdict(&mut out_rule, Verdict::Accept)?;
+
+ self.batch.add(&out_rule, nftnl::MsgType::Add)?;
Ok(())
}
- fn reset_policy(&mut self) -> Result<()> {
- if let Err(error) = self.dns_settings.reset() {
- warn!("Failed to reset DNS settings: {}", error.display_chain());
+ fn add_dns_rule(
+ &mut self,
+ tunnel: &tunnel::TunnelMetadata,
+ protocol: net::TransportProtocol,
+ ) -> Result<()> {
+ let mut rule = Rule::new(&self.out_chain)?;
+ rule.add_expr(nft_expr!(meta oifname))?;
+ rule.add_expr(nft_expr!(cmp == tunnel_iface_name(tunnel)))?;
+
+ check_port(&mut rule, protocol, End::Dst, 53)?;
+
+ check_l3proto(&mut rule, IpAddr::V4(tunnel.gateway))?;
+
+ rule.add_expr(nft_expr!(payload ipv4 daddr))?;
+ rule.add_expr(nft_expr!(cmp != tunnel.gateway))?;
+
+ add_verdict(&mut rule, Verdict::Drop)?;
+
+ self.batch.add(&rule, nftnl::MsgType::Add)?;
+ Ok(())
+ }
+
+ fn add_allow_tunnel_rules(&mut self, tunnel: &tunnel::TunnelMetadata) -> Result<()> {
+ let tunnel_interface = tunnel_iface_name(tunnel);
+ self.batch.add(
+ &allow_interface_rule(&self.out_chain, Direction::Out, &tunnel_interface)?,
+ nftnl::MsgType::Add,
+ )?;
+ self.batch.add(
+ &allow_interface_rule(&self.in_chain, Direction::In, &tunnel_interface)?,
+ nftnl::MsgType::Add,
+ )?;
+ Ok(())
+ }
+
+ fn add_allow_lan_rules(&mut self) -> Result<()> {
+ // LAN -> LAN
+ for chain in &[&self.in_chain, &self.out_chain] {
+ for net in &*super::PRIVATE_NETS {
+ let mut rule = Rule::new(chain)?;
+ check_net(&mut rule, End::Src, IpNetwork::V4(*net))?;
+ check_net(&mut rule, End::Dst, IpNetwork::V4(*net))?;
+
+ add_verdict(&mut rule, Verdict::Accept)?;
+
+ self.batch.add(&rule, nftnl::MsgType::Add)?;
+ }
+ }
+ // LAN -> multicast
+ for net in &*super::PRIVATE_NETS {
+ let mut rule = Rule::new(&self.out_chain)?;
+ check_net(&mut rule, End::Src, IpNetwork::V4(*net))?;
+ check_net(&mut rule, End::Dst, IpNetwork::V4(*super::MULTICAST_NET))?;
+
+ add_verdict(&mut rule, Verdict::Accept)?;
+
+ self.batch.add(&rule, nftnl::MsgType::Add)?;
}
Ok(())
}
}
+
+fn allow_dhcp_rule<'a>(chain: &'a Chain, direction: Direction) -> Result<Rule<'a>> {
+ const PROTOCOL: net::TransportProtocol = net::TransportProtocol::Udp;
+ const SERVER_PORT: u16 = 67;
+ const CLIENT_PORT: u16 = 68;
+ let broadcast_addr = IpAddr::V4(Ipv4Addr::new(255, 255, 255, 255));
+
+ let mut rule = Rule::new(&chain)?;
+
+ match direction {
+ Direction::In => {
+ check_port(&mut rule, PROTOCOL, End::Src, SERVER_PORT)?;
+ check_port(&mut rule, PROTOCOL, End::Dst, CLIENT_PORT)?;
+ }
+ Direction::Out => {
+ check_port(&mut rule, PROTOCOL, End::Src, CLIENT_PORT)?;
+ check_port(&mut rule, PROTOCOL, End::Dst, SERVER_PORT)?;
+ check_ip(&mut rule, End::Dst, broadcast_addr)?;
+ }
+ }
+
+ add_verdict(&mut rule, Verdict::Accept)?;
+
+ Ok(rule)
+}
+
+fn allow_interface_rule<'a>(
+ chain: &'a Chain,
+ direction: Direction,
+ iface: &InterfaceName,
+) -> Result<Rule<'a>> {
+ let mut rule = Rule::new(&chain)?;
+ check_iface(&mut rule, direction, iface)?;
+ add_verdict(&mut rule, Verdict::Accept)?;
+
+ Ok(rule)
+}
+
+
+fn check_iface(rule: &mut Rule, direction: Direction, iface: &InterfaceName) -> Result<()> {
+ rule.add_expr(match direction {
+ Direction::In => nft_expr!(meta iifname),
+ Direction::Out => nft_expr!(meta oifname),
+ })?;
+ rule.add_expr(nft_expr!(cmp == iface))?;
+ Ok(())
+}
+
+fn check_net(rule: &mut Rule, end: End, net: IpNetwork) -> Result<()> {
+ // Must check network layer protocol before loading network layer payload
+ check_l3proto(rule, net.ip())?;
+
+ rule.add_expr(match (net, end) {
+ (IpNetwork::V4(_), End::Src) => nft_expr!(payload ipv4 saddr),
+ (IpNetwork::V4(_), End::Dst) => nft_expr!(payload ipv4 daddr),
+ (IpNetwork::V6(_), End::Src) => nft_expr!(payload ipv6 saddr),
+ (IpNetwork::V6(_), End::Dst) => nft_expr!(payload ipv6 daddr),
+ })?;
+ rule.add_expr(nft_expr!(bitwise mask net.mask(), xor 0))?;
+ rule.add_expr(nft_expr!(cmp == net.ip()))?;
+
+ Ok(())
+}
+
+fn check_endpoint(rule: &mut Rule, end: End, endpoint: &net::Endpoint) -> Result<()> {
+ check_ip(rule, end, endpoint.address.ip())?;
+ check_port(rule, endpoint.protocol, end, endpoint.address.port())?;
+ Ok(())
+}
+
+
+fn check_ip(rule: &mut Rule, end: End, ip: IpAddr) -> Result<()> {
+ // Must check network layer protocol before loading network layer payload
+ check_l3proto(rule, ip)?;
+
+ rule.add_expr(match (ip, end) {
+ (IpAddr::V4(..), End::Src) => nft_expr!(payload ipv4 saddr),
+ (IpAddr::V4(..), End::Dst) => nft_expr!(payload ipv4 daddr),
+ (IpAddr::V6(..), End::Src) => nft_expr!(payload ipv6 saddr),
+ (IpAddr::V6(..), End::Dst) => nft_expr!(payload ipv6 daddr),
+ })?;
+ match ip {
+ IpAddr::V4(addr) => rule.add_expr(nft_expr!(cmp == addr))?,
+ IpAddr::V6(addr) => rule.add_expr(nft_expr!(cmp == addr))?,
+ }
+ Ok(())
+}
+
+fn check_port(
+ rule: &mut Rule,
+ protocol: net::TransportProtocol,
+ end: End,
+ port: u16,
+) -> Result<()> {
+ // Must check transport layer protocol before loading transport layer payload
+ check_l4proto(rule, protocol)?;
+
+ rule.add_expr(match (protocol, end) {
+ (net::TransportProtocol::Udp, End::Src) => nft_expr!(payload udp sport),
+ (net::TransportProtocol::Udp, End::Dst) => nft_expr!(payload udp dport),
+ (net::TransportProtocol::Tcp, End::Src) => nft_expr!(payload tcp sport),
+ (net::TransportProtocol::Tcp, End::Dst) => nft_expr!(payload tcp dport),
+ })?;
+ rule.add_expr(nft_expr!(cmp == port.to_be()))?;
+ Ok(())
+}
+
+fn tunnel_iface_name(tunnel: &tunnel::TunnelMetadata) -> InterfaceName {
+ InterfaceName::Exact(CString::new(&tunnel.interface[..]).unwrap())
+}
+
+fn check_l3proto(rule: &mut Rule, ip: IpAddr) -> Result<()> {
+ rule.add_expr(nft_expr!(meta nfproto))?;
+ rule.add_expr(nft_expr!(cmp == l3proto(ip)))?;
+ Ok(())
+}
+
+fn l3proto(addr: IpAddr) -> u8 {
+ match addr {
+ IpAddr::V4(_) => libc::NFPROTO_IPV4 as u8,
+ IpAddr::V6(_) => libc::NFPROTO_IPV6 as u8,
+ }
+}
+
+fn check_l4proto(rule: &mut Rule, protocol: net::TransportProtocol) -> Result<()> {
+ rule.add_expr(nft_expr!(meta l4proto))?;
+ rule.add_expr(nft_expr!(cmp == l4proto(protocol)))?;
+ Ok(())
+}
+
+fn l4proto(protocol: net::TransportProtocol) -> u8 {
+ match protocol {
+ net::TransportProtocol::Udp => libc::IPPROTO_UDP as u8,
+ net::TransportProtocol::Tcp => libc::IPPROTO_TCP as u8,
+ }
+}
+
+fn add_verdict(rule: &mut Rule, verdict: expr::Verdict) -> Result<()> {
+ if *ADD_COUNTERS {
+ rule.add_expr(nft_expr!(counter))?;
+ }
+ Ok(rule.add_expr(verdict)?)
+}
diff --git a/talpid-core/src/firewall/macos/mod.rs b/talpid-core/src/firewall/macos/mod.rs
index 3ee77a22ac..c4cb25f15b 100644
--- a/talpid-core/src/firewall/macos/mod.rs
+++ b/talpid-core/src/firewall/macos/mod.rs
@@ -21,6 +21,8 @@ error_chain! {
}
}
+/// TODO(linus): This crate is not supposed to be Mullvad-aware. So at some point this should be
+/// replaced by allowing the anchor name to be configured from the public API of this crate.
const ANCHOR_NAME: &'static str = "mullvad";
/// The macOS firewall implementation. Acting as converter between the `Firewall` trait API
diff --git a/talpid-core/src/firewall/mod.rs b/talpid-core/src/firewall/mod.rs
index 78befd68af..3b5aa04703 100644
--- a/talpid-core/src/firewall/mod.rs
+++ b/talpid-core/src/firewall/mod.rs
@@ -1,6 +1,8 @@
-use std::path::Path;
+#[cfg(unix)]
use ipnetwork::Ipv4Network;
+#[cfg(unix)]
use std::net::Ipv4Addr;
+use std::path::Path;
use talpid_types::net::Endpoint;
#[cfg(unix)]
diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs
index be46de881b..a043538442 100644
--- a/talpid-core/src/lib.rs
+++ b/talpid-core/src/lib.rs
@@ -31,8 +31,9 @@ extern crate openvpn_plugin;
extern crate talpid_ipc;
extern crate talpid_types;
-#[cfg(windows)]
-extern crate libc;
+#[cfg(target_os = "linux")]
+#[macro_use]
+extern crate nftnl;
/// Working with processes.
pub mod process;