From 5d78ab4d08a2c4b541470781ae17cd52b09750a0 Mon Sep 17 00:00:00 2001 From: lafleur Date: Wed, 27 Oct 2021 22:11:51 +0200 Subject: add working Match trait to Rule --- examples/firewall.rs | 142 ++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/rule_match.rs | 214 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 examples/firewall.rs create mode 100644 src/rule_match.rs diff --git a/examples/firewall.rs b/examples/firewall.rs new file mode 100644 index 0000000..e3ee54d --- /dev/null +++ b/examples/firewall.rs @@ -0,0 +1,142 @@ +use rustables::{Batch, FinalizedBatch, Chain, Hook, Match, MatchError, Policy, Rule, Protocol, ProtoFamily, Table, MsgType, expr::LogGroup}; +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), +} + +const TABLE_NAME: &str = "main-table"; + +fn main() -> Result<(), Error> { + let mut fw = Firewall::new()?; + fw.start()?; + Ok(()) +} + +pub struct Firewall { + table: Rc +} + +impl Firewall { + pub fn new() -> Result { + let table = Table::new( + &CString::new(TABLE_NAME)?, + ProtoFamily::Inet + ); + Ok(Firewall { table: Rc::new(table) }) + } + /// Attempt to use the batch from the struct holding the table. + pub fn allow_port(&mut self, port: &str, protocol: &Protocol, chain: Rc, batch: &mut Batch) -> Result<(), Error> { + let rule = Rule::new(chain).dport(port, protocol)?.accept().add_to_batch(batch); + batch.add(&rule, MsgType::Add); + Ok(()) + } + /// If there is no batch applied, apply the current realm's batch. + pub fn start(&mut self) -> Result<(), Error> { + let mut batch = Batch::new(); + batch.add(&self.table, MsgType::Add); + + let local_net = IpNetwork::new([192, 168, 1, 0].into(), 24).unwrap(); + let mut inbound = Chain::new(&CString::new("in")?, Rc::clone(&self.table)); + inbound.set_hook(Hook::In, 0); + inbound.set_policy(Policy::Drop); + let inbound = Rc::new(inbound); + batch.add(&inbound, MsgType::Add); + let mut outbound = Chain::new(&CString::new("out")?, Rc::clone(&self.table)); + outbound.set_hook(Hook::Out, 0); + outbound.set_policy(Policy::Accept); + batch.add(&outbound, MsgType::Add); + let mut forward = Chain::new(&CString::new("forward")?, Rc::clone(&self.table)); + forward.set_hook(Hook::Forward, 0); + forward.set_policy(Policy::Accept); + batch.add(&forward, MsgType::Add); + Rule::new(Rc::clone(&inbound)) + .established() + .accept() + .add_to_batch(&mut batch); + Rule::new(Rc::clone(&inbound)) + .iface("lo")? + .accept() + .add_to_batch(&mut batch); + self.allow_port("22", &Protocol::TCP, Rc::clone(&inbound), &mut batch)?; + Rule::new(Rc::clone(&inbound)) + .dport("80", &Protocol::TCP)? + .snetwork(local_net) + .accept() + .add_to_batch(&mut batch); + Rule::new(Rc::clone(&inbound)) + .icmp() + .accept() + .add_to_batch(&mut batch); + Rule::new(Rc::clone(&inbound)) + .igmp() + .drop() + .add_to_batch(&mut batch); + + //use nftnl::expr::LogPrefix; + //let prefix = "REALM=".to_string() + &self.realm_def.name; + Rule::new(Rc::clone(&inbound)) + .log(Some(LogGroup(1)), None) + //.log( Some(LogGroup::LogGroupOne), Some(LogPrefix::new(&prefix) + // .expect("Could not convert log prefix string to CString"))) + .add_to_batch(&mut batch); + + // Chain is defined over a Table, as is Batch, so we can never borrow them at the same + // time. The next statement would fail. + //self.allow_port("22", &Protocol::TCP, &inbound); + + let finalized_batch = batch.finalize().unwrap(); + apply_nftnl_batch(finalized_batch)?; + println!("ruleset applied"); + Ok(()) + } + /// If there are any rulesets applied, remove them. + pub fn stop(&mut self) -> Result<(), Error> { + let table = Table::new(&CString::new(TABLE_NAME)?, ProtoFamily::Inet); + let mut batch = Batch::new(); + batch.add(&table, MsgType::Add); + batch.add(&table, MsgType::Del); + Ok(()) + } +} + +fn apply_nftnl_batch(mut nftnl_finalized_batch: FinalizedBatch) + -> Result<(), std::io::Error> { + let socket = mnl::Socket::new(mnl::Bus::Netfilter)?; + socket.send_all(&mut nftnl_finalized_batch)?; + // Parse results from the socket : + let portid = socket.portid(); + let mut buffer = vec![0; rustables::nft_nlmsg_maxsize() as usize]; + // Unclear variable : + let seq = 0; + loop { + let length = socket.recv(&mut buffer[..])?; + if length == 0 { + eprintln!("batch socket returned 0"); + break; + } + match mnl::cb_run(&buffer[..length], seq, portid)? { + mnl::CbResult::Stop => { + break; + } + mnl::CbResult::Ok => (), + } + } + Ok(()) +} + diff --git a/src/lib.rs b/src/lib.rs index 6eedf9f..c42c5a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -116,6 +116,9 @@ pub use rule::Rule; #[cfg(feature = "query")] pub use rule::{get_rules_cb, list_rules_for_chain}; +mod rule_match; +pub use rule_match::{Match, Protocol, 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_match.rs b/src/rule_match.rs new file mode 100644 index 0000000..1cf71d9 --- /dev/null +++ b/src/rule_match.rs @@ -0,0 +1,214 @@ +use crate::{Batch, Rule, nft_expr, rustables_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), +} + + +/// 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 Match trait over [`rustables::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, Match, Protocol, Rule, ProtoFamily, 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 mut inbound = Chain::new(&CString::new("inbound").unwrap(), table); +/// inbound.set_hook(Hook::In, 0); +/// let inbound = Rc::new(inbound); +/// batch.add(&inbound, MsgType::Add); +/// let rule = Rule::new(inbound) +/// .dport("80", &Protocol::TCP).unwrap() +/// .accept(); +/// batch.add(&rule, MsgType::Add); +/// ``` +pub trait Match { + /// 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 + where Self: std::marker::Sized; + /// Match packets on `protocol`. + fn protocol(self, protocol: Protocol) -> Result + 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 `interface`. `iface_name` is an interface name, as in + /// "wlan0" or "lo". + fn iface(self, ifacename: &str) -> Result + 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, prefix: Option) -> 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 `rustables::Rule`. +impl Match 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.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::()?.to_be())); + Ok(self) + } + fn protocol(mut self, protocol: Protocol) -> Result { + 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(mut self, iface_name: &str) -> Result { + let iface_index = iface_index(iface_name)?; + self.add_expr(&nft_expr!(meta iif)); + self.add_expr(&nft_expr!(cmp == iface_index)); + 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, prefix: Option) -> 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. +fn iface_index(name: &str) -> Result { + let c_name = CString::new(name)?; + let index = unsafe { libc::if_nametoindex(c_name.as_ptr()) }; + match index { + 0 => Err(Error::NoSuchIface), + _ => Ok(index) + } +} + + -- cgit v1.2.3 From 1f8797d6891303be93530ad6538cd2e8bd8782e7 Mon Sep 17 00:00:00 2001 From: lafleur Date: Fri, 5 Nov 2021 11:39:55 +0100 Subject: firewall example syntax typo --- examples/firewall.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/examples/firewall.rs b/examples/firewall.rs index e3ee54d..edff0b0 100644 --- a/examples/firewall.rs +++ b/examples/firewall.rs @@ -34,19 +34,19 @@ pub struct Firewall { impl Firewall { pub fn new() -> Result { - let table = Table::new( - &CString::new(TABLE_NAME)?, - ProtoFamily::Inet - ); - Ok(Firewall { table: Rc::new(table) }) + let table = Rc::new(Table::new( + &CString::new(TABLE_NAME)?, + ProtoFamily::Inet + )); + Ok(Firewall { table }) } /// Attempt to use the batch from the struct holding the table. pub fn allow_port(&mut self, port: &str, protocol: &Protocol, chain: Rc, batch: &mut Batch) -> Result<(), Error> { - let rule = Rule::new(chain).dport(port, protocol)?.accept().add_to_batch(batch); - batch.add(&rule, MsgType::Add); - Ok(()) + let rule = Rule::new(chain).dport(port, protocol)?.accept().add_to_batch(batch); + batch.add(&rule, MsgType::Add); + Ok(()) } - /// If there is no batch applied, apply the current realm's batch. + /// Apply the current realm's batch. pub fn start(&mut self) -> Result<(), Error> { let mut batch = Batch::new(); batch.add(&self.table, MsgType::Add); @@ -92,14 +92,10 @@ impl Firewall { //let prefix = "REALM=".to_string() + &self.realm_def.name; Rule::new(Rc::clone(&inbound)) .log(Some(LogGroup(1)), None) - //.log( Some(LogGroup::LogGroupOne), Some(LogPrefix::new(&prefix) + //.log( Some(LogGroup(1)), Some(LogPrefix::new(&prefix) // .expect("Could not convert log prefix string to CString"))) .add_to_batch(&mut batch); - // Chain is defined over a Table, as is Batch, so we can never borrow them at the same - // time. The next statement would fail. - //self.allow_port("22", &Protocol::TCP, &inbound); - let finalized_batch = batch.finalize().unwrap(); apply_nftnl_batch(finalized_batch)?; println!("ruleset applied"); -- cgit v1.2.3 From faf7693637393caa3d274d406677a42eaa824073 Mon Sep 17 00:00:00 2001 From: lafleur Date: Mon, 8 Nov 2021 14:57:19 +0100 Subject: use feature query in tests --- examples/firewall.rs | 34 ++++++---------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/examples/firewall.rs b/examples/firewall.rs index edff0b0..f852618 100644 --- a/examples/firewall.rs +++ b/examples/firewall.rs @@ -1,4 +1,5 @@ -use rustables::{Batch, FinalizedBatch, Chain, Hook, Match, MatchError, Policy, Rule, Protocol, ProtoFamily, Table, MsgType, expr::LogGroup}; +use rustables::{Batch, Chain, Hook, Match, MatchError, Policy, Rule, Protocol, ProtoFamily, Table, MsgType, expr::LogGroup}; +use rustables::query::{send_batch, Error as QueryError}; use ipnetwork::IpNetwork; use std::ffi::{CString, NulError}; use std::str::Utf8Error; @@ -18,6 +19,8 @@ pub enum Error { Utf8Error(#[from] Utf8Error), #[error("Error applying batch")] BatchError(#[from] std::io::Error), + #[error("Error applying batch")] + QueryError(#[from] QueryError), } const TABLE_NAME: &str = "main-table"; @@ -96,8 +99,8 @@ impl Firewall { // .expect("Could not convert log prefix string to CString"))) .add_to_batch(&mut batch); - let finalized_batch = batch.finalize().unwrap(); - apply_nftnl_batch(finalized_batch)?; + let mut finalized_batch = batch.finalize().unwrap(); + send_batch(&mut finalized_batch)?; println!("ruleset applied"); Ok(()) } @@ -111,28 +114,3 @@ impl Firewall { } } -fn apply_nftnl_batch(mut nftnl_finalized_batch: FinalizedBatch) - -> Result<(), std::io::Error> { - let socket = mnl::Socket::new(mnl::Bus::Netfilter)?; - socket.send_all(&mut nftnl_finalized_batch)?; - // Parse results from the socket : - let portid = socket.portid(); - let mut buffer = vec![0; rustables::nft_nlmsg_maxsize() as usize]; - // Unclear variable : - let seq = 0; - loop { - let length = socket.recv(&mut buffer[..])?; - if length == 0 { - eprintln!("batch socket returned 0"); - break; - } - match mnl::cb_run(&buffer[..length], seq, portid)? { - mnl::CbResult::Stop => { - break; - } - mnl::CbResult::Ok => (), - } - } - Ok(()) -} - -- cgit v1.2.3 From d951b70f7e0417c3bcfc9d9b356aadfbfbcd4696 Mon Sep 17 00:00:00 2001 From: lafleur Date: Mon, 8 Nov 2021 15:18:17 +0100 Subject: fix dev 'query' feature being pulled in release build --- Cargo.toml | 4 +++- src/rule_match.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 34be66d..7df5b6c 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,10 @@ thiserror = "1.0" log = "0.4" libc = "0.2.43" mnl = "0.2" +ipnetwork = "0.16" [dev-dependencies] -ipnetwork = "0.16" +rustables = { path = ".", features = ["query"] } [build-dependencies] bindgen = "0.53.1" diff --git a/src/rule_match.rs b/src/rule_match.rs index 1cf71d9..35c71b6 100644 --- a/src/rule_match.rs +++ b/src/rule_match.rs @@ -1,4 +1,4 @@ -use crate::{Batch, Rule, nft_expr, rustables_sys::libc}; +use crate::{Batch, Rule, nft_expr, sys::libc}; use crate::expr::{LogGroup, LogPrefix}; use ipnetwork::IpNetwork; use std::ffi::{CString, NulError}; -- cgit v1.2.3 From 445d3e153f6673888123081f5024c3e402c370ef Mon Sep 17 00:00:00 2001 From: lafleur Date: Tue, 9 Nov 2021 13:34:00 +0100 Subject: move iface() to iface_id() and add new iface() to Rule --- src/lib.rs | 2 +- src/rule_match.rs | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c42c5a2..984eec7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,7 +117,7 @@ pub use rule::Rule; pub use rule::{get_rules_cb, list_rules_for_chain}; mod rule_match; -pub use rule_match::{Match, Protocol, Error as MatchError}; +pub use rule_match::{iface_index, Match, Protocol, Error as MatchError}; pub mod set; diff --git a/src/rule_match.rs b/src/rule_match.rs index 35c71b6..f60f8c0 100644 --- a/src/rule_match.rs +++ b/src/rule_match.rs @@ -18,6 +18,8 @@ pub enum Error { NoSuchIface, #[error("Error converting from a string to an integer")] ParseError(#[from] ParseIntError), + #[error("the interface name is too long")] + NameTooLong, } @@ -62,9 +64,12 @@ pub trait Match { 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 `interface`. `iface_name` is an interface name, as in - /// "wlan0" or "lo". - fn iface(self, ifacename: &str) -> Result + /// Match packets going through `iface_index`. Interface indexes can be queried with + /// `iface_index()`. + fn iface_id(self, iface_index: libc::c_uint) -> Result + 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 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. @@ -130,12 +135,20 @@ impl Match for Rule { self.add_expr(&nft_expr!(cmp != 0u32)); self } - fn iface(mut self, iface_name: &str) -> Result { - let iface_index = iface_index(iface_name)?; + fn iface_id(mut self, iface_index: libc::c_uint) -> Result { 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 { + if iface_name.len() > libc::IFNAMSIZ { + return Err(Error::NameTooLong); + } + + self.add_expr(&nft_expr!(meta iifname)); + self.add_expr(&nft_expr!(cmp == CString::new(iface_name)?.as_bytes())); + Ok(self) + } fn saddr(mut self, ip: IpAddr) -> Self { self.add_expr(&nft_expr!(meta nfproto)); match ip { @@ -202,7 +215,7 @@ impl Match for Rule { } /// Look up the interface index for a given interface name. -fn iface_index(name: &str) -> Result { +pub fn iface_index(name: &str) -> Result { let c_name = CString::new(name)?; let index = unsafe { libc::if_nametoindex(c_name.as_ptr()) }; match index { -- cgit v1.2.3 From cce17e810b1c4e277e998f0c01491fe4156a5a2f Mon Sep 17 00:00:00 2001 From: lafleur Date: Tue, 9 Nov 2021 16:42:30 +0100 Subject: rule_match => rule_methods --- src/lib.rs | 4 +- src/rule_match.rs | 227 ---------------------------------------------------- src/rule_methods.rs | 227 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 229 deletions(-) delete mode 100644 src/rule_match.rs create mode 100644 src/rule_methods.rs diff --git a/src/lib.rs b/src/lib.rs index 984eec7..33bf12c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -116,8 +116,8 @@ pub use rule::Rule; #[cfg(feature = "query")] pub use rule::{get_rules_cb, list_rules_for_chain}; -mod rule_match; -pub use rule_match::{iface_index, Match, Protocol, Error as MatchError}; +mod rule_methods; +pub use rule_methods::{iface_index, Protocol, RuleMethods, Error as MatchError}; pub mod set; diff --git a/src/rule_match.rs b/src/rule_match.rs deleted file mode 100644 index f60f8c0..0000000 --- a/src/rule_match.rs +++ /dev/null @@ -1,227 +0,0 @@ -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 Match trait over [`rustables::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, Match, Protocol, Rule, ProtoFamily, 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 mut inbound = Chain::new(&CString::new("inbound").unwrap(), table); -/// inbound.set_hook(Hook::In, 0); -/// let inbound = Rc::new(inbound); -/// batch.add(&inbound, MsgType::Add); -/// let rule = Rule::new(inbound) -/// .dport("80", &Protocol::TCP).unwrap() -/// .accept(); -/// batch.add(&rule, MsgType::Add); -/// ``` -pub trait Match { - /// 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 - where Self: std::marker::Sized; - /// Match packets on `protocol`. - fn protocol(self, protocol: Protocol) -> Result - 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 - 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 - 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, prefix: Option) -> 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 `rustables::Rule`. -impl Match 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.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::()?.to_be())); - Ok(self) - } - fn protocol(mut self, protocol: Protocol) -> Result { - 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.add_expr(&nft_expr!(meta iif)); - self.add_expr(&nft_expr!(cmp == iface_index)); - Ok(self) - } - fn iface(mut self, iface_name: &str) -> Result { - if iface_name.len() > libc::IFNAMSIZ { - return Err(Error::NameTooLong); - } - - self.add_expr(&nft_expr!(meta iifname)); - self.add_expr(&nft_expr!(cmp == CString::new(iface_name)?.as_bytes())); - 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, prefix: Option) -> 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 { - let c_name = CString::new(name)?; - let index = unsafe { libc::if_nametoindex(c_name.as_ptr()) }; - match index { - 0 => Err(Error::NoSuchIface), - _ => Ok(index) - } -} - - diff --git a/src/rule_methods.rs b/src/rule_methods.rs new file mode 100644 index 0000000..91b2c8a --- /dev/null +++ b/src/rule_methods.rs @@ -0,0 +1,227 @@ +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 [`rustables::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, 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 mut inbound = Chain::new(&CString::new("inbound").unwrap(), table); +/// inbound.set_hook(Hook::In, 0); +/// let inbound = Rc::new(inbound); +/// batch.add(&inbound, MsgType::Add); +/// let rule = Rule::new(inbound) +/// .dport("80", &Protocol::TCP).unwrap() +/// .accept(); +/// batch.add(&rule, MsgType::Add); +/// ``` +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 + where Self: std::marker::Sized; + /// Match packets on `protocol`. + fn protocol(self, protocol: Protocol) -> Result + 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 + 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 + 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, prefix: Option) -> 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 `rustables::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.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::()?.to_be())); + Ok(self) + } + fn protocol(mut self, protocol: Protocol) -> Result { + 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.add_expr(&nft_expr!(meta iif)); + self.add_expr(&nft_expr!(cmp == iface_index)); + Ok(self) + } + fn iface(mut self, iface_name: &str) -> Result { + if iface_name.len() > libc::IFNAMSIZ { + return Err(Error::NameTooLong); + } + + self.add_expr(&nft_expr!(meta iifname)); + self.add_expr(&nft_expr!(cmp == CString::new(iface_name)?.as_bytes())); + 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, prefix: Option) -> 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 { + let c_name = CString::new(name)?; + let index = unsafe { libc::if_nametoindex(c_name.as_ptr()) }; + match index { + 0 => Err(Error::NoSuchIface), + _ => Ok(index) + } +} + + -- cgit v1.2.3 From 96e543c77c13cb1c2eb9b1220f1fc5c510deef6b Mon Sep 17 00:00:00 2001 From: lafleur Date: Tue, 9 Nov 2021 19:41:32 +0100 Subject: sample ChainMethods impl, updated example --- Cargo.toml | 1 + examples/firewall.rs | 140 ++++++++++++++++++++++++++++++--------------------- src/chain_methods.rs | 78 ++++++++++++++++++++++++++++ src/lib.rs | 3 ++ 4 files changed, 165 insertions(+), 57 deletions(-) create mode 100644 src/chain_methods.rs diff --git a/Cargo.toml b/Cargo.toml index 7df5b6c..e2a2039 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ log = "0.4" libc = "0.2.43" mnl = "0.2" ipnetwork = "0.16" +serde = { version = "1.0", features = ["derive"] } [dev-dependencies] rustables = { path = ".", features = ["query"] } diff --git a/examples/firewall.rs b/examples/firewall.rs index f852618..0b5ea0c 100644 --- a/examples/firewall.rs +++ b/examples/firewall.rs @@ -1,10 +1,13 @@ -use rustables::{Batch, Chain, Hook, Match, MatchError, Policy, Rule, Protocol, ProtoFamily, Table, MsgType, expr::LogGroup}; +use rustables::{Batch, Chain, ChainMethods, Direction, MatchError, ProtoFamily, + Protocol, Rule, RuleMethods, Table, MsgType, Verdict}; 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")] @@ -21,96 +24,119 @@ pub enum Error { 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 mut fw = Firewall::new()?; + let fw = Firewall::new()?; fw.start()?; Ok(()) } + +/// An example firewall. See the source of its `start()` method. pub struct Firewall { - table: Rc
+ batch: Batch, + inbound: Rc, + _outbound: Rc, + _forward: Rc, + _table: Rc
, } impl Firewall { pub fn new() -> Result { - let table = Rc::new(Table::new( - &CString::new(TABLE_NAME)?, - ProtoFamily::Inet - )); - Ok(Firewall { table }) - } - /// Attempt to use the batch from the struct holding the table. - pub fn allow_port(&mut self, port: &str, protocol: &Protocol, chain: Rc, batch: &mut Batch) -> Result<(), Error> { - let rule = Rule::new(chain).dport(port, protocol)?.accept().add_to_batch(batch); - batch.add(&rule, MsgType::Add); - Ok(()) - } - /// Apply the current realm's batch. - pub fn start(&mut self) -> Result<(), Error> { let mut batch = Batch::new(); - batch.add(&self.table, MsgType::Add); + let _table = Rc::new( + Table::new(&CString::new(TABLE_NAME)?, ProtoFamily::Inet) + ); + batch.add(&_table, MsgType::Add); - let local_net = IpNetwork::new([192, 168, 1, 0].into(), 24).unwrap(); - let mut inbound = Chain::new(&CString::new("in")?, Rc::clone(&self.table)); - inbound.set_hook(Hook::In, 0); - inbound.set_policy(Policy::Drop); - let inbound = Rc::new(inbound); - batch.add(&inbound, MsgType::Add); - let mut outbound = Chain::new(&CString::new("out")?, Rc::clone(&self.table)); - outbound.set_hook(Hook::Out, 0); - outbound.set_policy(Policy::Accept); - batch.add(&outbound, MsgType::Add); - let mut forward = Chain::new(&CString::new("forward")?, Rc::clone(&self.table)); - forward.set_hook(Hook::Forward, 0); - forward.set_policy(Policy::Accept); - batch.add(&forward, MsgType::Add); - Rule::new(Rc::clone(&inbound)) + // Create base chains. Base chains are hooked into a Direction/Hook. + let inbound = Rc::new( + Chain::from_direction(&Direction::Inbound, Rc::clone(&_table))? + .verdict(&Verdict::Drop) + .add_to_batch(&mut batch) + ); + let _outbound = Rc::new( + Chain::from_direction(&Direction::Outbound, Rc::clone(&_table))? + .verdict(&Verdict::Accept) + .add_to_batch(&mut batch) + ); + let _forward = Rc::new( + Chain::from_direction(&Direction::Forward, Rc::clone(&_table))? + .verdict(&Verdict::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 batch); - Rule::new(Rc::clone(&inbound)) + .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 batch); - self.allow_port("22", &Protocol::TCP, Rc::clone(&inbound), &mut batch)?; - Rule::new(Rc::clone(&inbound)) + .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 batch); - Rule::new(Rc::clone(&inbound)) + .add_to_batch(&mut self.batch); + + // Allow ICMP traffic, drop IGMP. + Rule::new(Rc::clone(&self.inbound)) .icmp() .accept() - .add_to_batch(&mut batch); - Rule::new(Rc::clone(&inbound)) + .add_to_batch(&mut self.batch); + Rule::new(Rc::clone(&self.inbound)) .igmp() .drop() - .add_to_batch(&mut batch); + .add_to_batch(&mut self.batch); - //use nftnl::expr::LogPrefix; - //let prefix = "REALM=".to_string() + &self.realm_def.name; - Rule::new(Rc::clone(&inbound)) + // Log all traffic not accepted to NF_LOG group 1, accessible with ulogd. + Rule::new(Rc::clone(&self.inbound)) .log(Some(LogGroup(1)), None) - //.log( Some(LogGroup(1)), Some(LogPrefix::new(&prefix) - // .expect("Could not convert log prefix string to CString"))) - .add_to_batch(&mut batch); + .add_to_batch(&mut self.batch); - let mut finalized_batch = batch.finalize().unwrap(); + let mut finalized_batch = self.batch.finalize().unwrap(); send_batch(&mut finalized_batch)?; - println!("ruleset applied"); + println!("table {} commited", TABLE_NAME); Ok(()) } - /// If there are any rulesets applied, remove them. - pub fn stop(&mut self) -> Result<(), Error> { - let table = Table::new(&CString::new(TABLE_NAME)?, ProtoFamily::Inet); - let mut batch = Batch::new(); - batch.add(&table, MsgType::Add); - batch.add(&table, MsgType::Del); + /// 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..81b5fd1 --- /dev/null +++ b/src/chain_methods.rs @@ -0,0 +1,78 @@ +use crate::{Batch, Chain, Hook, MsgType, Policy, Table}; +use std::ffi::{CString, NulError}; +use std::rc::Rc; + +use serde::{Deserialize, Serialize}; + +/// A helper trait over [`rustables::Chain`]. +pub trait ChainMethods { + /// Create a new Chain instance from a [`Direction`] over a [`rustables::Table`]. + fn from_direction(direction: &Direction, table: Rc
) -> Result where Self: std::marker::Sized; + /// Add a [`Verdict`] to the current Chain. + fn verdict(self, verdict: &Verdict) -> Self; + fn add_to_batch(self, batch: &mut Batch) -> Self; +} + +impl ChainMethods for Chain { + fn from_direction(direction: &Direction, table: Rc
) -> Result { + let chain_name = CString::new(direction.display())?; + let mut chain = Chain::new(&chain_name, table); + chain.set_hook(direction.get_hook(), 0); + Ok(chain) + } + fn verdict(mut self, verdict: &Verdict) -> Self { + self.set_policy(verdict.get()); + self + } + fn add_to_batch(self, batch: &mut Batch) -> Self { + batch.add(&self, MsgType::Add); + self + } +} + +/// A Serializable wrapper type around [`rustables::Hook`]. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum Direction { + Inbound, + Outbound, + Forward +} +impl Direction { + /// Return the Direction's [`rustables::Hook`], ie its representation inside rustables. Note that + /// there are Hooks not represented here, namely Prerouting and Postrouting. File a bug if + /// you need those. + pub fn get_hook(&self) -> Hook { + match self { + Direction::Inbound => Hook::In, + Direction::Outbound => Hook::Out, + Direction::Forward => Hook::Forward, + } + } + /// Return a string representation of the Direction. + pub fn display(&self) -> String { + let s = match self { + Direction::Inbound => "inbound", + Direction::Outbound => "outbound", + Direction::Forward => "forward", + }; + s.to_string() + } +} +/// A Serializable wrapper type around [`rustables::Policy`]. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum Verdict { + Accept, + Drop +} +impl Verdict { + /// Return the rustables representation of a Verdict (ie, a [`rustables::Policy`]). + pub fn get(&self) -> Policy { + match self { + Verdict::Accept => Policy::Accept, + Verdict::Drop => Policy::Drop, + } + } +} + diff --git a/src/lib.rs b/src/lib.rs index 33bf12c..511b0ba 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, Direction, Verdict}; + pub mod query; mod rule; -- cgit v1.2.3 From 5264331441e625d26bedd6ce9b5995713e4b5c57 Mon Sep 17 00:00:00 2001 From: lafleur Date: Tue, 9 Nov 2021 20:24:31 +0100 Subject: use native types in chain_methods --- examples/firewall.rs | 28 +++++++++---------- src/chain_methods.rs | 78 ++++++++++++++-------------------------------------- src/lib.rs | 2 +- 3 files changed, 35 insertions(+), 73 deletions(-) diff --git a/examples/firewall.rs b/examples/firewall.rs index 0b5ea0c..46a0a4d 100644 --- a/examples/firewall.rs +++ b/examples/firewall.rs @@ -1,5 +1,5 @@ -use rustables::{Batch, Chain, ChainMethods, Direction, MatchError, ProtoFamily, - Protocol, Rule, RuleMethods, Table, MsgType, Verdict}; +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; @@ -44,36 +44,36 @@ pub struct Firewall { inbound: Rc, _outbound: Rc, _forward: Rc, - _table: Rc
, + table: Rc
, } impl Firewall { pub fn new() -> Result { let mut batch = Batch::new(); - let _table = Rc::new( + let table = Rc::new( Table::new(&CString::new(TABLE_NAME)?, ProtoFamily::Inet) ); - batch.add(&_table, MsgType::Add); + batch.add(&table, MsgType::Add); // Create base chains. Base chains are hooked into a Direction/Hook. let inbound = Rc::new( - Chain::from_direction(&Direction::Inbound, Rc::clone(&_table))? - .verdict(&Verdict::Drop) + Chain::from_hook(Hook::In, Rc::clone(&table)) + .verdict(Policy::Drop) .add_to_batch(&mut batch) ); let _outbound = Rc::new( - Chain::from_direction(&Direction::Outbound, Rc::clone(&_table))? - .verdict(&Verdict::Accept) + Chain::from_hook(Hook::Out, Rc::clone(&table)) + .verdict(Policy::Accept) .add_to_batch(&mut batch) ); let _forward = Rc::new( - Chain::from_direction(&Direction::Forward, Rc::clone(&_table))? - .verdict(&Verdict::Accept) + Chain::from_hook(Hook::Forward, Rc::clone(&table)) + .verdict(Policy::Accept) .add_to_batch(&mut batch) ); Ok(Firewall { - _table, + table, batch, inbound, _outbound, @@ -129,8 +129,8 @@ impl Firewall { } /// 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); + 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)?; diff --git a/src/chain_methods.rs b/src/chain_methods.rs index 81b5fd1..8259d13 100644 --- a/src/chain_methods.rs +++ b/src/chain_methods.rs @@ -1,27 +1,35 @@ use crate::{Batch, Chain, Hook, MsgType, Policy, Table}; -use std::ffi::{CString, NulError}; +use std::ffi::CString; use std::rc::Rc; -use serde::{Deserialize, Serialize}; /// A helper trait over [`rustables::Chain`]. pub trait ChainMethods { - /// Create a new Chain instance from a [`Direction`] over a [`rustables::Table`]. - fn from_direction(direction: &Direction, table: Rc
) -> Result where Self: std::marker::Sized; - /// Add a [`Verdict`] to the current Chain. - fn verdict(self, verdict: &Verdict) -> Self; + /// Create a new Chain instance from a [`rustables::Hook`] over a [`rustables::Table`]. + fn from_hook(hook: Hook, table: Rc
) -> Self + where Self: std::marker::Sized; + /// Add a [`rustables::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_direction(direction: &Direction, table: Rc
) -> Result { - let chain_name = CString::new(direction.display())?; + fn from_hook(hook: Hook, table: Rc
) -> 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(direction.get_hook(), 0); - Ok(chain) + chain.set_hook(hook, 0); + chain } - fn verdict(mut self, verdict: &Verdict) -> Self { - self.set_policy(verdict.get()); + fn verdict(mut self, policy: Policy) -> Self { + self.set_policy(policy); self } fn add_to_batch(self, batch: &mut Batch) -> Self { @@ -30,49 +38,3 @@ impl ChainMethods for Chain { } } -/// A Serializable wrapper type around [`rustables::Hook`]. -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] -#[serde(rename_all = "snake_case")] -pub enum Direction { - Inbound, - Outbound, - Forward -} -impl Direction { - /// Return the Direction's [`rustables::Hook`], ie its representation inside rustables. Note that - /// there are Hooks not represented here, namely Prerouting and Postrouting. File a bug if - /// you need those. - pub fn get_hook(&self) -> Hook { - match self { - Direction::Inbound => Hook::In, - Direction::Outbound => Hook::Out, - Direction::Forward => Hook::Forward, - } - } - /// Return a string representation of the Direction. - pub fn display(&self) -> String { - let s = match self { - Direction::Inbound => "inbound", - Direction::Outbound => "outbound", - Direction::Forward => "forward", - }; - s.to_string() - } -} -/// A Serializable wrapper type around [`rustables::Policy`]. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "snake_case")] -pub enum Verdict { - Accept, - Drop -} -impl Verdict { - /// Return the rustables representation of a Verdict (ie, a [`rustables::Policy`]). - pub fn get(&self) -> Policy { - match self { - Verdict::Accept => Policy::Accept, - Verdict::Drop => Policy::Drop, - } - } -} - diff --git a/src/lib.rs b/src/lib.rs index 511b0ba..5cf9ca6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,7 +110,7 @@ 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, Direction, Verdict}; +pub use chain_methods::ChainMethods; pub mod query; -- cgit v1.2.3 From 880ec583d3a5d1750806961298d88d028c1980d3 Mon Sep 17 00:00:00 2001 From: lafleur Date: Tue, 9 Nov 2021 20:36:51 +0100 Subject: fix iface(), docs --- src/chain_methods.rs | 6 +++--- src/rule_methods.rs | 24 +++++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/chain_methods.rs b/src/chain_methods.rs index 8259d13..c41e085 100644 --- a/src/chain_methods.rs +++ b/src/chain_methods.rs @@ -3,12 +3,12 @@ use std::ffi::CString; use std::rc::Rc; -/// A helper trait over [`rustables::Chain`]. +/// A helper trait over [`crate::Chain`]. pub trait ChainMethods { - /// Create a new Chain instance from a [`rustables::Hook`] over a [`rustables::Table`]. + /// Create a new Chain instance from a [`crate::Hook`] over a [`crate::Table`]. fn from_hook(hook: Hook, table: Rc
) -> Self where Self: std::marker::Sized; - /// Add a [`rustables::Policy`] to the current Chain. + /// Add a [`crate::Policy`] to the current Chain. fn verdict(self, policy: Policy) -> Self; fn add_to_batch(self, batch: &mut Batch) -> Self; } diff --git a/src/rule_methods.rs b/src/rule_methods.rs index 91b2c8a..9a8ef58 100644 --- a/src/rule_methods.rs +++ b/src/rule_methods.rs @@ -31,25 +31,23 @@ pub enum Protocol { UDP } -/// A RuleMethods trait over [`rustables::Rule`], to make it match some criteria, and give it a +/// 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, Protocol, ProtoFamily, Rule, RuleMethods, Table, MsgType, Hook}; +/// 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 mut inbound = Chain::new(&CString::new("inbound").unwrap(), table); -/// inbound.set_hook(Hook::In, 0); -/// let inbound = Rc::new(inbound); -/// batch.add(&inbound, 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(); -/// batch.add(&rule, MsgType::Add); +/// .accept() +/// .add_to_batch(&mut batch); /// ``` pub trait RuleMethods { /// Match ICMP packets. @@ -86,7 +84,7 @@ pub trait RuleMethods { fn add_to_batch(self, batch: &mut Batch) -> Self; } -/// A trait to add helper functions to match some criterium over `rustables::Rule`. +/// 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)); @@ -141,12 +139,16 @@ impl RuleMethods for Rule { Ok(self) } fn iface(mut self, iface_name: &str) -> Result { - if iface_name.len() > libc::IFNAMSIZ { + 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 == CString::new(iface_name)?.as_bytes())); + self.add_expr(&nft_expr!(cmp == name_arr.as_ref())); Ok(self) } fn saddr(mut self, ip: IpAddr) -> Self { -- cgit v1.2.3