diff options
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | examples/firewall.rs | 142 | ||||
-rw-r--r-- | src/chain_methods.rs | 40 | ||||
-rw-r--r-- | src/lib.rs | 6 | ||||
-rw-r--r-- | src/rule_methods.rs | 229 |
5 files changed, 421 insertions, 1 deletions
@@ -1,6 +1,7 @@ [package] name = "rustables" version = "0.7.0" +resolver = "2" authors = ["lafleur@boum.org, Simon Thoby, Mullvad VPN"] license = "GPL-3.0-or-later" description = "Safe abstraction for libnftnl. Provides low-level userspace access to the in-kernel nf_tables subsystem" @@ -20,9 +21,11 @@ thiserror = "1.0" log = "0.4" libc = "0.2.43" mnl = "0.2" +ipnetwork = "0.16" +serde = { version = "1.0", features = ["derive"] } [dev-dependencies] -ipnetwork = "0.16" +rustables = { path = ".", features = ["query"] } [build-dependencies] bindgen = "0.53.1" diff --git a/examples/firewall.rs b/examples/firewall.rs new file mode 100644 index 0000000..46a0a4d --- /dev/null +++ b/examples/firewall.rs @@ -0,0 +1,142 @@ +use rustables::{Batch, Chain, ChainMethods, Hook, MatchError, ProtoFamily, + Protocol, Rule, RuleMethods, Table, MsgType, Policy}; +use rustables::query::{send_batch, Error as QueryError}; +use rustables::expr::{LogGroup, LogPrefix, LogPrefixError}; +use ipnetwork::IpNetwork; +use std::ffi::{CString, NulError}; +use std::str::Utf8Error; +use std::rc::Rc; + + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Unable to open netlink socket to netfilter")] + NetlinkOpenError(#[source] std::io::Error), + #[error("Firewall is already started")] + AlreadyDone, + #[error("Error converting from a C String")] + NulError(#[from] NulError), + #[error("Error creating match")] + MatchError(#[from] MatchError), + #[error("Error converting to utf-8 string")] + Utf8Error(#[from] Utf8Error), + #[error("Error applying batch")] + BatchError(#[from] std::io::Error), + #[error("Error applying batch")] + QueryError(#[from] QueryError), + #[error("Error encoding the prefix")] + LogPrefixError(#[from] LogPrefixError), +} + +const TABLE_NAME: &str = "main-table"; + + +fn main() -> Result<(), Error> { + let fw = Firewall::new()?; + fw.start()?; + Ok(()) +} + + +/// An example firewall. See the source of its `start()` method. +pub struct Firewall { + batch: Batch, + inbound: Rc<Chain>, + _outbound: Rc<Chain>, + _forward: Rc<Chain>, + table: Rc<Table>, +} + +impl Firewall { + pub fn new() -> Result<Self, Error> { + let mut batch = Batch::new(); + let table = Rc::new( + Table::new(&CString::new(TABLE_NAME)?, ProtoFamily::Inet) + ); + batch.add(&table, MsgType::Add); + + // Create base chains. Base chains are hooked into a Direction/Hook. + let inbound = Rc::new( + Chain::from_hook(Hook::In, Rc::clone(&table)) + .verdict(Policy::Drop) + .add_to_batch(&mut batch) + ); + let _outbound = Rc::new( + Chain::from_hook(Hook::Out, Rc::clone(&table)) + .verdict(Policy::Accept) + .add_to_batch(&mut batch) + ); + let _forward = Rc::new( + Chain::from_hook(Hook::Forward, Rc::clone(&table)) + .verdict(Policy::Accept) + .add_to_batch(&mut batch) + ); + + Ok(Firewall { + table, + batch, + inbound, + _outbound, + _forward + }) + } + /// Allow some common-sense exceptions to inbound drop, and accept outbound and forward. + pub fn start(mut self) -> Result<(), Error> { + // Allow all established connections to get in. + Rule::new(Rc::clone(&self.inbound)) + .established() + .accept() + .add_to_batch(&mut self.batch); + // Allow all traffic on the loopback interface. + Rule::new(Rc::clone(&self.inbound)) + .iface("lo")? + .accept() + .add_to_batch(&mut self.batch); + // Allow ssh from anywhere, and log to dmesg with a prefix. + Rule::new(Rc::clone(&self.inbound)) + .dport("22", &Protocol::TCP)? + .accept() + .log(None, Some(LogPrefix::new("allow ssh connection:")?)) + .add_to_batch(&mut self.batch); + + // Allow http from all IPs in 192.168.1.255/24 . + let local_net = IpNetwork::new([192, 168, 1, 0].into(), 24).unwrap(); + Rule::new(Rc::clone(&self.inbound)) + .dport("80", &Protocol::TCP)? + .snetwork(local_net) + .accept() + .add_to_batch(&mut self.batch); + + // Allow ICMP traffic, drop IGMP. + Rule::new(Rc::clone(&self.inbound)) + .icmp() + .accept() + .add_to_batch(&mut self.batch); + Rule::new(Rc::clone(&self.inbound)) + .igmp() + .drop() + .add_to_batch(&mut self.batch); + + // Log all traffic not accepted to NF_LOG group 1, accessible with ulogd. + Rule::new(Rc::clone(&self.inbound)) + .log(Some(LogGroup(1)), None) + .add_to_batch(&mut self.batch); + + let mut finalized_batch = self.batch.finalize().unwrap(); + send_batch(&mut finalized_batch)?; + println!("table {} commited", TABLE_NAME); + Ok(()) + } + /// If there is any table with name TABLE_NAME, remove it. + pub fn stop(mut self) -> Result<(), Error> { + self.batch.add(&self.table, MsgType::Add); + self.batch.add(&self.table, MsgType::Del); + + let mut finalized_batch = self.batch.finalize().unwrap(); + send_batch(&mut finalized_batch)?; + println!("table {} destroyed", TABLE_NAME); + Ok(()) + } +} + + diff --git a/src/chain_methods.rs b/src/chain_methods.rs new file mode 100644 index 0000000..c41e085 --- /dev/null +++ b/src/chain_methods.rs @@ -0,0 +1,40 @@ +use crate::{Batch, Chain, Hook, MsgType, Policy, Table}; +use std::ffi::CString; +use std::rc::Rc; + + +/// A helper trait over [`crate::Chain`]. +pub trait ChainMethods { + /// Create a new Chain instance from a [`crate::Hook`] over a [`crate::Table`]. + fn from_hook(hook: Hook, table: Rc<Table>) -> Self + where Self: std::marker::Sized; + /// Add a [`crate::Policy`] to the current Chain. + fn verdict(self, policy: Policy) -> Self; + fn add_to_batch(self, batch: &mut Batch) -> Self; +} + + +impl ChainMethods for Chain { + fn from_hook(hook: Hook, table: Rc<Table>) -> Self { + let chain_name = match hook { + Hook::PreRouting => "prerouting", + Hook::Out => "out", + Hook::PostRouting => "postrouting", + Hook::Forward => "forward", + Hook::In => "in", + }; + let chain_name = CString::new(chain_name).unwrap(); + let mut chain = Chain::new(&chain_name, table); + chain.set_hook(hook, 0); + chain + } + fn verdict(mut self, policy: Policy) -> Self { + self.set_policy(policy); + self + } + fn add_to_batch(self, batch: &mut Batch) -> Self { + batch.add(&self, MsgType::Add); + self + } +} + @@ -109,6 +109,9 @@ mod chain; pub use chain::{get_chains_cb, list_chains_for_table}; pub use chain::{Chain, ChainType, Hook, Policy, Priority}; +mod chain_methods; +pub use chain_methods::ChainMethods; + pub mod query; mod rule; @@ -116,6 +119,9 @@ pub use rule::Rule; #[cfg(feature = "query")] pub use rule::{get_rules_cb, list_rules_for_chain}; +mod rule_methods; +pub use rule_methods::{iface_index, Protocol, RuleMethods, Error as MatchError}; + pub mod set; /// The type of the message as it's sent to netfilter. A message consists of an object, such as a diff --git a/src/rule_methods.rs b/src/rule_methods.rs new file mode 100644 index 0000000..9a8ef58 --- /dev/null +++ b/src/rule_methods.rs @@ -0,0 +1,229 @@ +use crate::{Batch, Rule, nft_expr, sys::libc}; +use crate::expr::{LogGroup, LogPrefix}; +use ipnetwork::IpNetwork; +use std::ffi::{CString, NulError}; +use std::net::IpAddr; +use std::num::ParseIntError; + + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Unable to open netlink socket to netfilter")] + NetlinkOpenError(#[source] std::io::Error), + #[error("Firewall is already started")] + AlreadyDone, + #[error("Error converting from a C string to a string")] + CStringError(#[from] NulError), + #[error("no interface found under that name")] + NoSuchIface, + #[error("Error converting from a string to an integer")] + ParseError(#[from] ParseIntError), + #[error("the interface name is too long")] + NameTooLong, +} + + +/// Simple protocol description. Note that it does not implement other layer 4 protocols as +/// IGMP et al. See [`Rule::igmp`] for a workaround. +#[derive(Debug, Clone)] +pub enum Protocol { + TCP, + UDP +} + +/// A RuleMethods trait over [`crate::Rule`], to make it match some criteria, and give it a +/// verdict. Mostly adapted from [talpid-core's +/// firewall](https://github.com/mullvad/mullvadvpn-app/blob/d92376b4d1df9b547930c68aa9bae9640ff2a022/talpid-core/src/firewall/linux.rs). +/// All methods return the rule itself, allowing them to be chained. Usage example : +/// ```rust +/// use rustables::{Batch, Chain, ChainMethods, Protocol, ProtoFamily, Rule, RuleMethods, Table, MsgType, Hook}; +/// use std::ffi::CString; +/// use std::rc::Rc; +/// let table = Rc::new(Table::new(&CString::new("main_table").unwrap(), ProtoFamily::Inet)); +/// let mut batch = Batch::new(); +/// batch.add(&table, MsgType::Add); +/// let inbound = Rc::new(Chain::from_hook(Hook::In, Rc::clone(&table)) +/// .add_to_batch(&mut batch)); +/// let rule = Rule::new(inbound) +/// .dport("80", &Protocol::TCP).unwrap() +/// .accept() +/// .add_to_batch(&mut batch); +/// ``` +pub trait RuleMethods { + /// Match ICMP packets. + fn icmp(self) -> Self; + /// Match IGMP packets. + fn igmp(self) -> Self; + /// Match packets to destination `port` and `protocol`. + fn dport(self, port: &str, protocol: &Protocol) -> Result<Self, Error> + where Self: std::marker::Sized; + /// Match packets on `protocol`. + fn protocol(self, protocol: Protocol) -> Result<Self, Error> + where Self: std::marker::Sized; + /// Match packets in an already established connections. + fn established(self) -> Self where Self: std::marker::Sized; + /// Match packets going through `iface_index`. Interface indexes can be queried with + /// `iface_index()`. + fn iface_id(self, iface_index: libc::c_uint) -> Result<Self, Error> + where Self: std::marker::Sized; + /// Match packets going through `iface_name`, an interface name, as in "wlan0" or "lo". + fn iface(self, iface_name: &str) -> Result<Self, Error> + where Self: std::marker::Sized; + /// Add a log instruction to the rule. `group` is the NFLog group, `prefix` is a prefix + /// appended to each log line. + fn log(self, group: Option<LogGroup>, prefix: Option<LogPrefix>) -> Self; + /// Match packets whose source IP address is `saddr`. + fn saddr(self, ip: IpAddr) -> Self; + /// Match packets whose source network is `snet`. + fn snetwork(self, ip: IpNetwork) -> Self; + /// Add the `Accept` verdict to the rule. The packet will be sent to destination. + fn accept(self) -> Self; + /// Add the `Drop` verdict to the rule. The packet will be dropped. + fn drop(self) -> Self; + /// Append rule to `batch`. + fn add_to_batch(self, batch: &mut Batch) -> Self; +} + +/// A trait to add helper functions to match some criterium over `crate::Rule`. +impl RuleMethods for Rule { + fn icmp(mut self) -> Self { + self.add_expr(&nft_expr!(meta l4proto)); + //self.add_expr(&nft_expr!(cmp == libc::IPPROTO_ICMPV6 as u8)); + self.add_expr(&nft_expr!(cmp == libc::IPPROTO_ICMP as u8)); + self + } + fn igmp(mut self) -> Self { + self.add_expr(&nft_expr!(meta l4proto)); + self.add_expr(&nft_expr!(cmp == libc::IPPROTO_IGMP as u8)); + self + } + fn dport(mut self, port: &str, protocol: &Protocol) -> Result<Self, Error> { + self.add_expr(&nft_expr!(meta l4proto)); + match protocol { + &Protocol::TCP => { + self.add_expr(&nft_expr!(cmp == libc::IPPROTO_TCP as u8)); + self.add_expr(&nft_expr!(payload tcp dport)); + }, + &Protocol::UDP => { + self.add_expr(&nft_expr!(cmp == libc::IPPROTO_UDP as u8)); + self.add_expr(&nft_expr!(payload udp dport)); + } + } + // Convert the port to Big-Endian number spelling. + // See https://github.com/mullvad/mullvadvpn-app/blob/d92376b4d1df9b547930c68aa9bae9640ff2a022/talpid-core/src/firewall/linux.rs#L969 + self.add_expr(&nft_expr!(cmp == port.parse::<u16>()?.to_be())); + Ok(self) + } + fn protocol(mut self, protocol: Protocol) -> Result<Self, Error> { + self.add_expr(&nft_expr!(meta l4proto)); + match protocol { + Protocol::TCP => { + self.add_expr(&nft_expr!(cmp == libc::IPPROTO_TCP as u8)); + }, + Protocol::UDP => { + self.add_expr(&nft_expr!(cmp == libc::IPPROTO_UDP as u8)); + } + } + Ok(self) + } + fn established(mut self) -> Self { + let allowed_states = crate::expr::ct::States::ESTABLISHED.bits(); + self.add_expr(&nft_expr!(ct state)); + self.add_expr(&nft_expr!(bitwise mask allowed_states, xor 0u32)); + self.add_expr(&nft_expr!(cmp != 0u32)); + self + } + fn iface_id(mut self, iface_index: libc::c_uint) -> Result<Self, Error> { + self.add_expr(&nft_expr!(meta iif)); + self.add_expr(&nft_expr!(cmp == iface_index)); + Ok(self) + } + fn iface(mut self, iface_name: &str) -> Result<Self, Error> { + if iface_name.len() >= libc::IFNAMSIZ { + return Err(Error::NameTooLong); + } + + let mut name_arr = [0u8; libc::IFNAMSIZ]; + for (pos, i) in iface_name.bytes().enumerate() { + name_arr[pos] = i; + } + self.add_expr(&nft_expr!(meta iifname)); + self.add_expr(&nft_expr!(cmp == name_arr.as_ref())); + Ok(self) + } + fn saddr(mut self, ip: IpAddr) -> Self { + self.add_expr(&nft_expr!(meta nfproto)); + match ip { + IpAddr::V4(addr) => { + self.add_expr(&nft_expr!(cmp == libc::NFPROTO_IPV4 as u8)); + self.add_expr(&nft_expr!(payload ipv4 saddr)); + self.add_expr(&nft_expr!(cmp == addr)) + }, + IpAddr::V6(addr) => { + self.add_expr(&nft_expr!(cmp == libc::NFPROTO_IPV6 as u8)); + self.add_expr(&nft_expr!(payload ipv6 saddr)); + self.add_expr(&nft_expr!(cmp == addr)) + } + } + self + } + fn snetwork(mut self, net: IpNetwork) -> Self { + self.add_expr(&nft_expr!(meta nfproto)); + match net { + IpNetwork::V4(_) => { + self.add_expr(&nft_expr!(cmp == libc::NFPROTO_IPV4 as u8)); + self.add_expr(&nft_expr!(payload ipv4 saddr)); + self.add_expr(&nft_expr!(bitwise mask net.mask(), xor 0u32)); + self.add_expr(&nft_expr!(cmp == net.network())); + }, + IpNetwork::V6(_) => { + self.add_expr(&nft_expr!(cmp == libc::NFPROTO_IPV6 as u8)); + self.add_expr(&nft_expr!(payload ipv6 saddr)); + self.add_expr(&nft_expr!(bitwise mask net.mask(), xor &[0u16; 8][..])); + self.add_expr(&nft_expr!(cmp == net.network())); + } + } + self + } + fn log(mut self, group: Option<LogGroup>, prefix: Option<LogPrefix>) -> Self { + match (group.is_some(), prefix.is_some()) { + (true, true) => { + self.add_expr(&nft_expr!(log group group prefix prefix)); + }, + (false, true) => { + self.add_expr(&nft_expr!(log prefix prefix)); + }, + (true, false) => { + self.add_expr(&nft_expr!(log group group)); + }, + (false, false) => { + self.add_expr(&nft_expr!(log)); + } + } + self + } + fn accept(mut self) -> Self { + self.add_expr(&nft_expr!(verdict accept)); + self + } + fn drop(mut self) -> Self { + self.add_expr(&nft_expr!(verdict drop)); + self + } + fn add_to_batch(self, batch: &mut Batch) -> Self { + batch.add(&self, crate::MsgType::Add); + self + } +} + +/// Look up the interface index for a given interface name. +pub fn iface_index(name: &str) -> Result<libc::c_uint, Error> { + let c_name = CString::new(name)?; + let index = unsafe { libc::if_nametoindex(c_name.as_ptr()) }; + match index { + 0 => Err(Error::NoSuchIface), + _ => Ok(index) + } +} + + |