summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2026-02-27 12:04:24 +0100
committerMarkus Pettersson <markus.pettersson@mullvad.net>2026-02-27 14:23:34 +0100
commit052b6024a6c8046b76fbc5ea937702b5f023a72b (patch)
tree04ada266432d0f8016df44e0b2ea47c0490c9218
parent306220973979e30f637941614f7780863ef90ce6 (diff)
downloadmullvadvpn-nftables-json.tar.xz
mullvadvpn-nftables-json.zip
WIP Snapshot basic nftable batchnftables-json
-rw-r--r--Cargo.lock102
-rw-r--r--talpid-core/Cargo.toml3
-rw-r--r--talpid-core/src/firewall/linux.rs203
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(())
+ }
+}