diff options
| author | Linus Färnstrand <linus@mullvad.net> | 2018-06-26 15:33:46 +0200 |
|---|---|---|
| committer | Linus Färnstrand <linus@mullvad.net> | 2018-07-02 12:16:33 +0200 |
| commit | f04ffa0a8393d42f63813a71485ee043966417f1 (patch) | |
| tree | 1c39fa6f3908458ba5a1613d51f54cceb2be4596 | |
| parent | 0b7db4052b312e8f6d9d6458c8f34388f7746a4e (diff) | |
| download | mullvadvpn-f04ffa0a8393d42f63813a71485ee043966417f1.tar.xz mullvadvpn-f04ffa0a8393d42f63813a71485ee043966417f1.zip | |
Add Linux netfilter firewall integration
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 7 | ||||
| -rw-r--r-- | talpid-core/src/firewall/linux/mod.rs | 436 | ||||
| -rw-r--r-- | talpid-core/src/firewall/macos/mod.rs | 2 | ||||
| -rw-r--r-- | talpid-core/src/firewall/mod.rs | 4 | ||||
| -rw-r--r-- | talpid-core/src/lib.rs | 5 |
6 files changed, 447 insertions, 13 deletions
@@ -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; |
