diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a4dabe7..c9b4180 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: # Keep MSRV in sync with rust-version in Cargo.toml - rust: [stable, beta, nightly, 1.56.0] + rust: [stable, beta, nightly, 1.63.0] runs-on: ubuntu-latest steps: - name: Install build dependencies diff --git a/Cargo.lock b/Cargo.lock index f229dce..2086b35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.44" +version = "0.2.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10923947f84a519a45c8fefb7dd1b3e8c08747993381adee176d7a82b4195311" +checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" [[package]] name = "log" diff --git a/nftnl-sys/Cargo.toml b/nftnl-sys/Cargo.toml index 52ee0ff..8f7b9db 100644 --- a/nftnl-sys/Cargo.toml +++ b/nftnl-sys/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" keywords = ["nftables", "nft", "firewall", "iptables", "netfilter"] categories = ["network-programming", "os::unix-apis", "external-ffi-bindings", "no-std"] edition = "2021" -rust-version = "1.56.0" +rust-version = "1.63.0" [features] @@ -22,7 +22,7 @@ nftnl-1-1-2 = ["nftnl-1-1-1"] [dependencies] cfg-if = "1.0" -libc = "0.2.44" +libc = "0.2.166" [build-dependencies] cfg-if = "1.0" diff --git a/nftnl/Cargo.toml b/nftnl/Cargo.toml index fff1237..fcb9ad5 100644 --- a/nftnl/Cargo.toml +++ b/nftnl/Cargo.toml @@ -9,7 +9,7 @@ readme = "../README.md" keywords = ["nftables", "nft", "firewall", "iptables", "netfilter"] categories = ["network-programming", "os::unix-apis", "api-bindings"] edition = "2021" -rust-version = "1.56.0" +rust-version = "1.63.0" [features] nftnl-1-0-7 = ["nftnl-sys/nftnl-1-0-7"] diff --git a/nftnl/examples/add-ingress-rule.rs b/nftnl/examples/add-ingress-rule.rs new file mode 100644 index 0000000..592e94b --- /dev/null +++ b/nftnl/examples/add-ingress-rule.rs @@ -0,0 +1,149 @@ +//! Adds a table, an ingress chain and some rules to netfilter. +//! +//! This example uses `verdict accept` everywhere. So even after running this the firewall won't +//! block anything. This is so anyone trying to run this does not end up in a strange state +//! where they don't understand why their network is broken. Try changing to `verdict drop` if +//! you want to see the block working. +//! +//! Run the following to print out current active tables, chains and rules in netfilter. Must be +//! executed as root: +//! ```bash +//! # nft list ruleset +//! ``` +//! After running this example, the output should be the following: +//! ```ignore +//! table inet example-table { +//! chain chain-for-ingress { +//! type filter hook ingress priority -450; policy accept; +//! ip saddr 127.0.0.1 counter packets 0 bytes 0 accept +//! } +//! } +//! ``` +//! +//! Try pinging any IP in the network range denoted by the outgoing rule and see the counter +//! increment: +//! ```bash +//! $ ping 127.0.0.2 +//! ``` +//! +//! Everything created by this example can be removed by running +//! ```bash +//! # nft delete table inet example-table +//! ``` + +use nftnl::{ + nft_expr, nftnl_sys::libc, Batch, Chain, ChainType, FinalizedBatch, Policy, ProtoFamily, Rule, + Table, +}; +use std::{ffi::CString, io, net::Ipv4Addr}; + +const TABLE_NAME: &str = "example-table"; +const CHAIN_NAME: &str = "chain-for-ingress"; + +fn main() -> Result<(), Box> { + // Create a batch. This is used to store all the netlink messages we will later send. + // Creating a new batch also automatically writes the initial batch begin message needed + // to tell netlink this is a single transaction that might arrive over multiple netlink packets. + let mut batch = Batch::new(); + + // Create a netfilter table operating on both IPv4 and IPv6 (ProtoFamily::Inet) + let table = Table::new(&CString::new(TABLE_NAME).unwrap(), ProtoFamily::Inet); + // Add the table to the batch with the `MsgType::Add` type, thus instructing netfilter to add + // this table under its `ProtoFamily::Inet` ruleset. + batch.add(&table, nftnl::MsgType::Add); + + // Create input and output chains under the table we created above. + let mut chain = Chain::new(&CString::new(CHAIN_NAME).unwrap(), &table); + + // Hook the chain to the input and output event hooks, with highest priority (priority zero). + // See the `Chain::set_hook` documentation for details. + chain.set_hook(nftnl::Hook::Ingress, -450); // -450 priority places this chain before any conntrack or defragmentation + + // Setting the chain type to filter is not necessary, as it is the default type. + chain.set_type(ChainType::Filter); + + // Ingress hooks need a device to bind to. + chain.set_device(&CString::new("lo").unwrap()); + + // Set the default policies on the chains. If no rule matches a packet processed by the + // `out_chain` or the `in_chain` it will accept the packet. + chain.set_policy(Policy::Accept); + + // Add the two chains to the batch with the `MsgType` to tell netfilter to create the chains + // under the table. + batch.add(&chain, nftnl::MsgType::Add); + + // === ADD A RULE ALLOWING (AND COUNTING) ALL PACKETS FROM THE 127.0.0.1 IP ADDRESS === + + let mut rule = Rule::new(&chain); + let local_ip = Ipv4Addr::new(127, 0, 0, 1); + + // Load the `nfproto` metadata into the netfilter register. This metadata denotes which layer3 + // protocol the packet being processed is using. + rule.add_expr(&nft_expr!(meta nfproto)); + // Check if the currently processed packet is an IPv4 packet. This must be done before payload + // data assuming the packet uses IPv4 can be loaded in the next expression. + rule.add_expr(&nft_expr!(cmp == libc::NFPROTO_IPV4 as u8)); + + // Load the IPv4 destination address into the netfilter register. + rule.add_expr(&nft_expr!(payload ipv4 saddr)); + // Compare the register with the IP we are interested in. + rule.add_expr(&nft_expr!(cmp == local_ip)); + + // Add a packet counter to the rule. Shows how many packets have been evaluated against this + // expression. Since expressions are evaluated from first to last, putting this counter before + // the above IP net check would make the counter increment on all packets also *not* matching + // those expressions. Because the counter would then be evaluated before it fails a check. + // Similarly, if the counter was added after the verdict it would always remain at zero. Since + // when the packet hits the verdict expression any further processing of expressions stop. + rule.add_expr(&nft_expr!(counter)); + + // Accept all the packets matching the rule so far. + rule.add_expr(&nft_expr!(verdict accept)); + + // Add the rule to the batch. Without this nothing would be sent over netlink and netfilter, + // and all the work on `block_out_to_private_net_rule` so far would go to waste. + batch.add(&rule, nftnl::MsgType::Add); + + // === FINALIZE THE TRANSACTION AND SEND THE DATA TO NETFILTER === + + // Finalize the batch. This means the batch end message is written into the batch, telling + // netfilter that we reached the end of the transaction message. It's also converted to a type + // that implements `IntoIterator`, thus allowing us to get the raw netlink data + // out so it can be sent over a netlink socket to netfilter. + let finalized_batch = batch.finalize(); + + // Send the entire batch and process any returned messages. + send_and_process(&finalized_batch)?; + Ok(()) +} + +fn send_and_process(batch: &FinalizedBatch) -> io::Result<()> { + // Create a netlink socket to netfilter. + let socket = mnl::Socket::new(mnl::Bus::Netfilter)?; + // Send all the bytes in the batch. + socket.send_all(batch)?; + + // Try to parse the messages coming back from netfilter. This part is still very unclear. + let portid = socket.portid(); + let mut buffer = vec![0; nftnl::nft_nlmsg_maxsize() as usize]; + let very_unclear_what_this_is_for = 2; + while let Some(message) = socket_recv(&socket, &mut buffer[..])? { + match mnl::cb_run(message, very_unclear_what_this_is_for, portid)? { + mnl::CbResult::Stop => { + break; + } + mnl::CbResult::Ok => (), + } + } + Ok(()) +} + +fn socket_recv<'a>(socket: &mnl::Socket, buf: &'a mut [u8]) -> io::Result> { + let ret = socket.recv(buf)?; + if ret > 0 { + Ok(Some(&buf[..ret])) + } else { + Ok(None) + } +} diff --git a/nftnl/src/chain.rs b/nftnl/src/chain.rs index 19431af..e2f433d 100644 --- a/nftnl/src/chain.rs +++ b/nftnl/src/chain.rs @@ -22,6 +22,8 @@ pub enum Hook { Out = libc::NF_INET_LOCAL_OUT as u16, /// Hook into the post-routing stage of netfilter. Corresponds to `NF_INET_POST_ROUTING`. PostRouting = libc::NF_INET_POST_ROUTING as u16, + /// Hook into the ingress stage of netfilter. Corresponds to `NF_INET_INGRESS`. + Ingress = libc::NF_INET_INGRESS as u16, } /// A chain policy. Decides what to do with a packet that was processed by the chain but did not @@ -133,6 +135,17 @@ impl<'a> Chain<'a> { } } + /// Sets the device for this chain. This only applies if the chain has been registered with an `ingress` hook by calling `set_hook`. + pub fn set_device>(&mut self, device: &T) { + unsafe { + sys::nftnl_chain_set_str( + self.chain, + sys::NFTNL_CHAIN_DEV as u16, + device.as_ref().as_ptr(), + ); + } + } + /// Returns the name of this chain. pub fn get_name(&self) -> &CStr { unsafe {