|
| 1 | +//! Adds a table, an ingress chain and some rules to netfilter. |
| 2 | +//! |
| 3 | +//! This example uses `verdict accept` everywhere. So even after running this the firewall won't |
| 4 | +//! block anything. This is so anyone trying to run this does not end up in a strange state |
| 5 | +//! where they don't understand why their network is broken. Try changing to `verdict drop` if |
| 6 | +//! you want to see the block working. |
| 7 | +//! |
| 8 | +//! Run the following to print out current active tables, chains and rules in netfilter. Must be |
| 9 | +//! executed as root: |
| 10 | +//! ```bash |
| 11 | +//! # nft list ruleset |
| 12 | +//! ``` |
| 13 | +//! After running this example, the output should be the following: |
| 14 | +//! ```ignore |
| 15 | +//! table inet example-table { |
| 16 | +//! chain chain-for-ingress { |
| 17 | +//! type filter hook ingress priority -450; policy accept; |
| 18 | +//! ip saddr 127.0.0.1 counter packets 0 bytes 0 accept |
| 19 | +//! } |
| 20 | +//! } |
| 21 | +//! ``` |
| 22 | +//! |
| 23 | +//! Try pinging any IP in the network range denoted by the outgoing rule and see the counter |
| 24 | +//! increment: |
| 25 | +//! ```bash |
| 26 | +//! $ ping 127.0.0.2 |
| 27 | +//! ``` |
| 28 | +//! |
| 29 | +//! Everything created by this example can be removed by running |
| 30 | +//! ```bash |
| 31 | +//! # nft delete table inet example-table |
| 32 | +//! ``` |
| 33 | +
|
| 34 | +use nftnl::{ |
| 35 | + nft_expr, nftnl_sys::libc, Batch, Chain, ChainType, FinalizedBatch, Policy, ProtoFamily, Rule, |
| 36 | + Table, |
| 37 | +}; |
| 38 | +use std::{ffi::CString, io, net::Ipv4Addr}; |
| 39 | + |
| 40 | +const TABLE_NAME: &str = "example-table"; |
| 41 | +const CHAIN_NAME: &str = "chain-for-ingress"; |
| 42 | + |
| 43 | +fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 44 | + // Create a batch. This is used to store all the netlink messages we will later send. |
| 45 | + // Creating a new batch also automatically writes the initial batch begin message needed |
| 46 | + // to tell netlink this is a single transaction that might arrive over multiple netlink packets. |
| 47 | + let mut batch = Batch::new(); |
| 48 | + |
| 49 | + // Create a netfilter table operating on both IPv4 and IPv6 (ProtoFamily::Inet) |
| 50 | + let table = Table::new(&CString::new(TABLE_NAME).unwrap(), ProtoFamily::Inet); |
| 51 | + // Add the table to the batch with the `MsgType::Add` type, thus instructing netfilter to add |
| 52 | + // this table under its `ProtoFamily::Inet` ruleset. |
| 53 | + batch.add(&table, nftnl::MsgType::Add); |
| 54 | + |
| 55 | + // Create input and output chains under the table we created above. |
| 56 | + let mut chain = Chain::new(&CString::new(CHAIN_NAME).unwrap(), &table); |
| 57 | + |
| 58 | + // Hook the chain to the input and output event hooks, with highest priority (priority zero). |
| 59 | + // See the `Chain::set_hook` documentation for details. |
| 60 | + chain.set_hook(nftnl::Hook::Ingress, -450); // -450 priority places this chain before any conntrack or defragmentation |
| 61 | + |
| 62 | + // Setting the chain type to filter is not necessary, as it is the default type. |
| 63 | + chain.set_type(ChainType::Filter); |
| 64 | + |
| 65 | + // Ingress hooks need a device to bind to. |
| 66 | + chain.set_device(&CString::new("lo").unwrap()); |
| 67 | + |
| 68 | + // Set the default policies on the chains. If no rule matches a packet processed by the |
| 69 | + // `out_chain` or the `in_chain` it will accept the packet. |
| 70 | + chain.set_policy(Policy::Accept); |
| 71 | + |
| 72 | + // Add the two chains to the batch with the `MsgType` to tell netfilter to create the chains |
| 73 | + // under the table. |
| 74 | + batch.add(&chain, nftnl::MsgType::Add); |
| 75 | + |
| 76 | + // === ADD A RULE ALLOWING (AND COUNTING) ALL PACKETS FROM THE 127.0.0.1 IP ADDRESS === |
| 77 | + |
| 78 | + let mut rule = Rule::new(&chain); |
| 79 | + let local_ip = Ipv4Addr::new(127, 0, 0, 1); |
| 80 | + |
| 81 | + // Load the `nfproto` metadata into the netfilter register. This metadata denotes which layer3 |
| 82 | + // protocol the packet being processed is using. |
| 83 | + rule.add_expr(&nft_expr!(meta nfproto)); |
| 84 | + // Check if the currently processed packet is an IPv4 packet. This must be done before payload |
| 85 | + // data assuming the packet uses IPv4 can be loaded in the next expression. |
| 86 | + rule.add_expr(&nft_expr!(cmp == libc::NFPROTO_IPV4 as u8)); |
| 87 | + |
| 88 | + // Load the IPv4 destination address into the netfilter register. |
| 89 | + rule.add_expr(&nft_expr!(payload ipv4 saddr)); |
| 90 | + // Compare the register with the IP we are interested in. |
| 91 | + rule.add_expr(&nft_expr!(cmp == local_ip)); |
| 92 | + |
| 93 | + // Add a packet counter to the rule. Shows how many packets have been evaluated against this |
| 94 | + // expression. Since expressions are evaluated from first to last, putting this counter before |
| 95 | + // the above IP net check would make the counter increment on all packets also *not* matching |
| 96 | + // those expressions. Because the counter would then be evaluated before it fails a check. |
| 97 | + // Similarly, if the counter was added after the verdict it would always remain at zero. Since |
| 98 | + // when the packet hits the verdict expression any further processing of expressions stop. |
| 99 | + rule.add_expr(&nft_expr!(counter)); |
| 100 | + |
| 101 | + // Accept all the packets matching the rule so far. |
| 102 | + rule.add_expr(&nft_expr!(verdict accept)); |
| 103 | + |
| 104 | + // Add the rule to the batch. Without this nothing would be sent over netlink and netfilter, |
| 105 | + // and all the work on `block_out_to_private_net_rule` so far would go to waste. |
| 106 | + batch.add(&rule, nftnl::MsgType::Add); |
| 107 | + |
| 108 | + // === FINALIZE THE TRANSACTION AND SEND THE DATA TO NETFILTER === |
| 109 | + |
| 110 | + // Finalize the batch. This means the batch end message is written into the batch, telling |
| 111 | + // netfilter that we reached the end of the transaction message. It's also converted to a type |
| 112 | + // that implements `IntoIterator<Item = &'a [u8]>`, thus allowing us to get the raw netlink data |
| 113 | + // out so it can be sent over a netlink socket to netfilter. |
| 114 | + let finalized_batch = batch.finalize(); |
| 115 | + |
| 116 | + // Send the entire batch and process any returned messages. |
| 117 | + send_and_process(&finalized_batch)?; |
| 118 | + Ok(()) |
| 119 | +} |
| 120 | + |
| 121 | +fn send_and_process(batch: &FinalizedBatch) -> io::Result<()> { |
| 122 | + // Create a netlink socket to netfilter. |
| 123 | + let socket = mnl::Socket::new(mnl::Bus::Netfilter)?; |
| 124 | + // Send all the bytes in the batch. |
| 125 | + socket.send_all(batch)?; |
| 126 | + |
| 127 | + // Try to parse the messages coming back from netfilter. This part is still very unclear. |
| 128 | + let portid = socket.portid(); |
| 129 | + let mut buffer = vec![0; nftnl::nft_nlmsg_maxsize() as usize]; |
| 130 | + let very_unclear_what_this_is_for = 2; |
| 131 | + while let Some(message) = socket_recv(&socket, &mut buffer[..])? { |
| 132 | + match mnl::cb_run(message, very_unclear_what_this_is_for, portid)? { |
| 133 | + mnl::CbResult::Stop => { |
| 134 | + break; |
| 135 | + } |
| 136 | + mnl::CbResult::Ok => (), |
| 137 | + } |
| 138 | + } |
| 139 | + Ok(()) |
| 140 | +} |
| 141 | + |
| 142 | +fn socket_recv<'a>(socket: &mnl::Socket, buf: &'a mut [u8]) -> io::Result<Option<&'a [u8]>> { |
| 143 | + let ret = socket.recv(buf)?; |
| 144 | + if ret > 0 { |
| 145 | + Ok(Some(&buf[..ret])) |
| 146 | + } else { |
| 147 | + Ok(None) |
| 148 | + } |
| 149 | +} |
0 commit comments