aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml5
-rw-r--r--examples/firewall.rs142
-rw-r--r--src/chain_methods.rs40
-rw-r--r--src/lib.rs6
-rw-r--r--src/rule_methods.rs229
5 files changed, 421 insertions, 1 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 34be66d..e2a2039 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
+ }
+}
+
diff --git a/src/lib.rs b/src/lib.rs
index 6eedf9f..5cf9ca6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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)
+ }
+}
+
+