diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2026-02-27 12:04:24 +0100 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2026-02-27 14:23:34 +0100 |
| commit | 052b6024a6c8046b76fbc5ea937702b5f023a72b (patch) | |
| tree | 04ada266432d0f8016df44e0b2ea47c0490c9218 | |
| parent | 306220973979e30f637941614f7780863ef90ce6 (diff) | |
| download | mullvadvpn-nftables-json.tar.xz mullvadvpn-nftables-json.zip | |
WIP Snapshot basic nftable batchnftables-json
| -rw-r--r-- | Cargo.lock | 102 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 3 | ||||
| -rw-r--r-- | talpid-core/src/firewall/linux.rs | 203 |
3 files changed, 305 insertions, 3 deletions
diff --git a/Cargo.lock b/Cargo.lock index 64a7c98627..2ad7cac68d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1137,6 +1137,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97af9b5f014e228b33e77d75ee0e6e87960124f0f4b16337b586a6bec91867b1" [[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] name = "dynosaur" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3345,6 +3351,21 @@ dependencies = [ ] [[package]] +name = "nftables" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c57e7343eed9e9330e084eef12651b15be3c8ed7825915a0ffa33736b852bed" +dependencies = [ + "schemars", + "serde", + "serde_json", + "serde_path_to_error", + "strum", + "strum_macros", + "thiserror 2.0.17", +] + +[[package]] name = "nftnl" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4398,6 +4419,26 @@ dependencies = [ ] [[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] name = "regex" version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4640,6 +4681,31 @@ dependencies = [ ] [[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.108", +] + +[[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4715,15 +4781,38 @@ dependencies = [ ] [[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", ] [[package]] @@ -5122,6 +5211,7 @@ dependencies = [ "log", "memoffset 0.6.5", "mnl", + "nftables", "nftnl", "nix 0.30.1", "parking_lot", @@ -7173,3 +7263,9 @@ dependencies = [ "quote", "syn 2.0.108", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 93853d82d3..355b09292e 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -24,6 +24,7 @@ tokio = { workspace = true, features = ["fs", "process", "rt-multi-thread"] } [dev-dependencies] insta = { workspace = true } +serde_json.workspace = true test-log = "0.2.17" tokio = { workspace = true, features = ["io-util", "test-util", "time"] } @@ -32,6 +33,7 @@ jnix = { version = "0.5.1", features = ["derive"] } [target.'cfg(target_os = "linux")'.dependencies] mnl = { version = "0.3.1", features = ["mnl-1-0-4"] } +nftables = "0.6.3" # NOTE: the libnftnl version specified in `features` # MUST match the version we link in /.cargo/config.toml nftnl = { version = "0.9.1", features = ["nftnl-1-2-0"] } @@ -103,6 +105,7 @@ features = [ [features] cgroup2 = [] multihop-pcap = ["talpid-wireguard/multihop-pcap"] +# nftables-json = ["dep:nftables"] wireguard-go = ["talpid-wireguard/wireguard-go"] [lints] diff --git a/talpid-core/src/firewall/linux.rs b/talpid-core/src/firewall/linux.rs index 4fcbdb7a61..7e246feb14 100644 --- a/talpid-core/src/firewall/linux.rs +++ b/talpid-core/src/firewall/linux.rs @@ -1,6 +1,7 @@ use super::{FirewallArguments, FirewallPolicy}; use crate::split_tunnel; use ipnetwork::IpNetwork; +use nftables; use nftnl::{ Batch, Chain, FinalizedBatch, ProtoFamily, Rule, Table, expr::{self, IcmpCode, Payload, RejectionType, Verdict}, @@ -150,6 +151,194 @@ impl Firewall { self.verify_tables(&[TABLE_NAME]) } + /// Generate a serializable [`FirewallPolicy`] for [`TABLE_NAME`] nftable using nftables-json. + pub fn policy(&mut self) -> Result<nftables::schema::Nftables<'_>> { + use nftables::{batch, expr, schema, stmt, types}; + let mut batch = batch::Batch::new(); + let table_name = "mullvad"; + let table = { + schema::NfListObject::Table(schema::Table { + family: types::NfFamily::INet, + name: "mullvad".into(), + ..Default::default() + }) + }; + // Create the table if it does not exist and clear it otherwise. + batch.add(table.clone()); + batch.delete(table.clone()); + batch.add(table.clone()); + + // Create batch [x] + // Add pre-routing chain. + let prerouting_chain = { + // prerouting_chain.set_hook(nftnl::Hook::PreRouting, PREROUTING_CHAIN_PRIORITY); + // prerouting_chain.set_type(nftnl::ChainType::Filter); + schema::NfListObject::Chain(schema::Chain { + name: "prerouting".into(), + table: table_name.into(), + hook: types::NfHook::Prerouting.into(), + prio: PREROUTING_CHAIN_PRIORITY.into(), + _type: types::NfChainType::Filter.into(), + ..schema::Chain::default() + }) + }; + batch.add(prerouting_chain); + + // Add output chain. + let output_chain = { + // let mut out_chain = Chain::new(OUT_CHAIN_NAME, table); + // out_chain.set_hook(nftnl::Hook::Out, 0); + // out_chain.set_policy(nftnl::Policy::Drop); + schema::NfListObject::Chain(schema::Chain { + name: "output".into(), + table: table_name.into(), + hook: types::NfHook::Output.into(), + prio: 0.into(), + policy: types::NfChainPolicy::Drop.into(), + ..schema::Chain::default() + }) + }; + batch.add(output_chain); + + // Add input chain. + let input_chain = { + // let mut in_chain = Chain::new(IN_CHAIN_NAME, table); + // in_chain.set_hook(nftnl::Hook::In, 0); + // in_chain.set_policy(nftnl::Policy::Drop); + schema::NfListObject::Chain(schema::Chain { + name: "input".into(), + table: table_name.into(), + hook: types::NfHook::Input.into(), + prio: 0.into(), + policy: types::NfChainPolicy::Drop.into(), + ..schema::Chain::default() + }) + }; + batch.add(input_chain); + + // Add forward chain. + let forward_chain = { + // let mut forward_chain = Chain::new(FORWARD_CHAIN_NAME, table); + // forward_chain.set_hook(nftnl::Hook::Forward, 0); + // forward_chain.set_policy(nftnl::Policy::Drop); + schema::NfListObject::Chain(schema::Chain { + name: "forward".into(), + table: table_name.into(), + hook: types::NfHook::Forward.into(), + prio: 0.into(), + policy: types::NfChainPolicy::Drop.into(), + ..schema::Chain::default() + }) + }; + batch.add(forward_chain); + + // Add mangle chain. + let mangle_chain = { + schema::NfListObject::Chain(schema::Chain { + // let mut mangle_chain = Chain::new(MANGLE_CHAIN_NAME, table); + name: "mangle".into(), + table: table_name.into(), + // mangle_chain.set_hook(nftnl::Hook::Out, MANGLE_CHAIN_PRIORITY); + hook: types::NfHook::Output.into(), + prio: MANGLE_CHAIN_PRIORITY.into(), + // mangle_chain.set_type(nftnl::ChainType::Route); + _type: types::NfChainType::Route.into(), + // mangle_chain.set_policy(nftnl::Policy::Accept); + policy: types::NfChainPolicy::Accept.into(), + ..schema::Chain::default() + }) + }; + batch.add(mangle_chain); + + // Add nat chain. + let nat_chain = { + schema::NfListObject::Chain(schema::Chain { + // let mut nat_chain = Chain::new(NAT_CHAIN_NAME, table); + name: "nat".into(), + table: table_name.into(), + // nat_chain.set_hook(nftnl::Hook::PostRouting, libc::NF_IP_PRI_NAT_SRC); + hook: types::NfHook::Postrouting.into(), + prio: libc::NF_IP_PRI_NAT_SRC.into(), + // nat_chain.set_type(nftnl::ChainType::Nat); + _type: types::NfChainType::NAT.into(), + // nat_chain.set_policy(nftnl::Policy::Accept); + policy: types::NfChainPolicy::Accept.into(), + ..schema::Chain::default() + }) + }; + batch.add(nat_chain); + + // TODO: Finalize batch [ ] + // Add loopback rules [] + let loopback_rules = { + const LOOPBACK_IFACE_NAME: &str = "lo"; + // Allow interface rules, direction: out + // let iface_index = crate::linux::iface_index(LOOPBACK_IFACE_NAME) + // .map_err(|e| Error::LookupIfaceIndexError(LOOPBACK_IFACE_NAME.to_owned(), e))?; + + let out = schema::NfListObject::Rule(schema::Rule { + chain: "output".into(), + comment: Some("Allow outgoing traffic on loopback".into()), + table: table_name.into(), + expr: vec![ + // // Direction::Out => nft_expr!(meta oif), + // expr::Meta::Oif, + // expr::Cmp::new(expr::CmpOp::Eq, iface_index), + stmt::Statement::Match(stmt::Match { + left: expr::Expression::Named(expr::NamedExpression::Meta(expr::Meta { + key: expr::MetaKey::Iifname, + })), + right: expr::Expression::String(LOOPBACK_IFACE_NAME.into()), + op: stmt::Operator::EQ, + }), + // TODO: Counters + stmt::Statement::Accept(None), + ] + .into(), + ..schema::Rule::default() + }); + + let _in = schema::NfListObject::Rule(schema::Rule { + chain: "input".into(), + comment: Some("Allow incoming traffic on loopback".into()), + table: table_name.into(), + expr: vec![ + // Direction::In => nft_expr!(meta iif), + // expr::Meta::Iif, + // expr::Cmp::new(expr::CmpOp::Eq, iface_index), + stmt::Statement::Match(stmt::Match { + left: expr::Expression::Named(expr::NamedExpression::Meta(expr::Meta { + key: expr::MetaKey::Oifname, + })), + right: expr::Expression::String(LOOPBACK_IFACE_NAME.into()), + op: stmt::Operator::EQ, + }), + // TODO: Counters + stmt::Statement::Accept(None), + ] + .into(), + ..schema::Rule::default() + }); + + // Allow interface rules, direction: in + vec![out, _in] + }; + for rule in loopback_rules { + batch.add(rule); + } + + // Add split-tunneling rules [] + // Add dhcp-client rules [] + // Add ndp rules [] + // Add policy-specific rules [] + + Ok(batch.to_nftables()) + // let batch = PolicyBatch::new(&table).finalize(&policy, self)?; + // Self::send_and_process(&batch)?; + // Self::apply_kernel_config(&policy); + // self.verify_tables(&[TABLE_NAME]) + } + /// Remove [`TABLE_NAME`] nftable. pub fn reset_policy(&mut self) -> Result<()> { let table = Table::new(TABLE_NAME, ProtoFamily::Inet); @@ -1176,3 +1365,17 @@ fn lock_down_arp_ignore_sysctl() -> io::Result<()> { } Ok(()) } + +mod test { + use super::*; + + /// Snapshot the standard set of firewall rules. + #[test] + fn create_batch() -> Result<()> { + let mut firewall = + Firewall::new(Default::default(), Default::default(), Default::default())?; + let policy = firewall.policy()?; + panic!("{}", serde_json::to_string_pretty(&policy).unwrap()); + Ok(()) + } +} |
