diff options
author | lafleur <lafleur@boum.org> | 2021-10-27 22:11:51 +0200 |
---|---|---|
committer | lafleur <lafleur@boum.org> | 2021-11-09 12:42:59 +0100 |
commit | 5d78ab4d08a2c4b541470781ae17cd52b09750a0 (patch) | |
tree | fbb82050f4964031e6e47196c864588c341e4eb0 | |
parent | e3fea7960d8a962bd25ac3d905d3b67aefc1fa49 (diff) |
add working Match trait to Rule
-rw-r--r-- | examples/firewall.rs | 142 | ||||
-rw-r--r-- | src/lib.rs | 3 | ||||
-rw-r--r-- | src/rule_match.rs | 214 |
3 files changed, 359 insertions, 0 deletions
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<Table> +} + +impl Firewall { + pub fn new() -> Result<Self, Error> { + 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<Chain>, 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(()) +} + @@ -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<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 `interface`. `iface_name` is an interface name, as in + /// "wlan0" or "lo". + fn iface(self, ifacename: &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 `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, 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(mut self, iface_name: &str) -> Result<Self, Error> { + 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<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. +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) + } +} + + |